Skip to content

Commit 9d27fd8

Browse files
authored
Merge pull request #2566 from strictdoc-project/stanislaw/node_field_origin
feat(commands/manage_autouid): rework the generation of MID/HASH
2 parents 072ce69 + c18e9f7 commit 9d27fd8

File tree

13 files changed

+259
-88
lines changed

13 files changed

+259
-88
lines changed

strictdoc/backend/sdoc/models/document_config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ def __init__(
102102
if enable_mid == "True"
103103
else (False if root == "False" else None)
104104
)
105-
self.relation_field: str = (
105+
self.relation_field: Optional[str] = (
106106
relation_field
107107
if relation_field is not None and len(relation_field) > 0
108-
else "UID"
108+
else None
109109
)
110110

111111
self.markup: Optional[str] = markup
@@ -168,6 +168,9 @@ def get_prefix(self) -> str:
168168
return self.requirement_prefix
169169
return "REQ-"
170170

171+
def get_relation_field(self) -> str:
172+
return self.relation_field or "UID"
173+
171174
def has_meta(self) -> bool:
172175
# TODO: When OPTIONS are not provided to a document, the self.number and
173176
# self.version are both None. Otherwise, they become empty strings "".

strictdoc/backend/sdoc/models/node.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
from collections import OrderedDict
6+
from enum import Enum
67
from typing import Any, Generator, List, Optional, Tuple, Union
78

89
from strictdoc.backend.sdoc.document_reference import DocumentReference
@@ -41,6 +42,15 @@ def __init__(self) -> None:
4142
self.title_number_string: Optional[str] = None
4243

4344

45+
class SDocNodeFieldOrigin(str, Enum):
46+
DOCUMENT = "DOCUMENT"
47+
SOURCE = "SOURCE"
48+
49+
@staticmethod
50+
def all() -> List[str]: # noqa: A003
51+
return list(map(lambda c: c.value, SDocNodeFieldOrigin))
52+
53+
4454
@auto_described
4555
class SDocNodeField:
4656
def __init__(
@@ -54,6 +64,7 @@ def __init__(
5464
self.field_name: str = field_name
5565
self.parts: List[Any] = parts
5666
self.multiline: bool = multiline__ is not None and len(multiline__) > 0
67+
self.origin: SDocNodeFieldOrigin = SDocNodeFieldOrigin.DOCUMENT
5768

5869
@staticmethod
5970
def create_from_string(
@@ -98,6 +109,12 @@ def get_text_value(self) -> str:
98109
raise NotImplementedError(part) # pragma: no cover
99110
return text
100111

112+
def is_document_origin(self) -> bool:
113+
return self.origin == SDocNodeFieldOrigin.DOCUMENT
114+
115+
def mark_as_source_origin(self) -> None:
116+
self.origin = SDocNodeFieldOrigin.SOURCE
117+
101118

102119
@auto_described
103120
class SDocNode(SDocNodeIF):
@@ -310,7 +327,7 @@ def reserved_uid(self) -> Optional[str]:
310327
config = assert_cast(document.config, DocumentConfig)
311328

312329
return self._get_cached_field(
313-
config.relation_field, singleline_only=True
330+
config.get_relation_field(), singleline_only=True
314331
)
315332

316333
@reserved_uid.setter
@@ -319,7 +336,7 @@ def reserved_uid(self, uid: Optional[str]) -> None:
319336
config = assert_cast(document.config, DocumentConfig)
320337

321338
self.set_field_value(
322-
field_name=config.relation_field,
339+
field_name=config.get_relation_field(),
323340
form_field_index=0,
324341
value=uid,
325342
)

strictdoc/backend/sdoc/writer.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def write_with_fragments(
123123
output += "\n"
124124

125125
enable_mid = document_config.enable_mid
126+
relation_field = document_config.relation_field
126127
markup = document_config.markup
127128
auto_levels_specified = document_config.ng_auto_levels_specified
128129
layout = document_config.layout
@@ -132,6 +133,7 @@ def write_with_fragments(
132133

133134
if (
134135
enable_mid is not None
136+
or relation_field is not None
135137
or markup is not None
136138
or auto_levels_specified
137139
or layout is not None
@@ -147,6 +149,11 @@ def write_with_fragments(
147149
output += "True" if enable_mid else "False"
148150
output += "\n"
149151

152+
if relation_field is not None:
153+
output += " RELATION_FIELD: "
154+
output += relation_field
155+
output += "\n"
156+
150157
if markup is not None:
151158
output += " MARKUP: "
152159
output += markup
@@ -408,7 +415,11 @@ def _print_requirement_fields(
408415
):
409416
continue
410417
fields = section_content.ordered_fields_lookup[field_name]
418+
411419
for field in fields:
420+
if not field.is_document_origin():
421+
continue
422+
412423
field_value = field.get_text_value()
413424
assert len(field_value) > 0
414425

strictdoc/backend/sdoc_source_code/models/source_node.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
from dataclasses import dataclass, field
6-
from typing import List, Optional, Union
6+
from typing import Any, List, Optional, Union
77

88
from strictdoc.backend.sdoc_source_code.models.function import Function
99
from strictdoc.backend.sdoc_source_code.models.function_range_marker import (
@@ -35,6 +35,8 @@ class SourceNode:
3535
fields: dict[str, str] = field(default_factory=dict)
3636
fields_locations: dict[str, tuple[int, int]] = field(default_factory=dict)
3737
function: Optional[Function] = None
38+
# FIXME: Adding SDocNode here causes circular import problem.
39+
sdoc_node: Optional[Any] = None
3840

3941
def get_sdoc_field(
4042
self,

strictdoc/commands/manage_autouid_command.py

Lines changed: 118 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import sys
2+
from typing import Dict, Optional
23

34
from strictdoc.backend.sdoc.errors.document_tree_error import DocumentTreeError
5+
from strictdoc.backend.sdoc.models.node import SDocNode
46
from strictdoc.backend.sdoc.writer import SDWriter
57
from strictdoc.backend.sdoc_source_code.marker_writer import MarkerWriter
68
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
@@ -16,7 +18,7 @@
1618
from strictdoc.core.traceability_index import TraceabilityIndex
1719
from strictdoc.core.traceability_index_builder import TraceabilityIndexBuilder
1820
from strictdoc.helpers.parallelizer import Parallelizer
19-
from strictdoc.helpers.sha256 import get_random_sha256, get_sha256
21+
from strictdoc.helpers.sha256 import get_random_sha256, get_sha256, is_sha256
2022
from strictdoc.helpers.string import (
2123
create_safe_acronym,
2224
)
@@ -97,6 +99,13 @@ def execute(
9799
)
98100
next_number += 1
99101

102+
for (
103+
trace_info_
104+
) in traceability_index.get_file_traceability_index().trace_infos:
105+
ManageAutoUIDCommand._rewrite_source_file(
106+
trace_info_, project_config
107+
)
108+
100109
for document in traceability_index.document_tree.document_list:
101110
assert document.meta is not None
102111

@@ -116,21 +125,15 @@ def execute(
116125
) as output_file:
117126
output_file.write(document_content)
118127

119-
for (
120-
trace_info_
121-
) in traceability_index.get_file_traceability_index().trace_infos:
122-
ManageAutoUIDCommand._rewrite_source_file(
123-
trace_info_, project_config
124-
)
125-
126128
@staticmethod
127129
def _rewrite_source_file(
128-
trace_info: SourceFileTraceabilityInfo, project_config: ProjectConfig
130+
trace_info: SourceFileTraceabilityInfo,
131+
project_config: ProjectConfig,
129132
) -> None:
130133
"""
131134
NOTE: This only updates the source code with the new calculated value.
132-
All links in the graph database and links in the search index are
133-
not modified for now.
135+
All links in the graph database and links in the search index
136+
ARE NOT! modified for now.
134137
"""
135138

136139
assert trace_info.source_file is not None
@@ -146,7 +149,19 @@ def _rewrite_source_file(
146149
with open(trace_info.source_file.full_path, "rb") as source_file_:
147150
file_bytes = source_file_.read()
148151

149-
rewrites = {}
152+
field_remapped_mid = "MID"
153+
154+
relevant_source_node_config = (
155+
project_config.get_relevant_source_nodes_entry(
156+
trace_info.source_file.in_doctree_source_file_rel_path_posix
157+
)
158+
)
159+
if relevant_source_node_config is not None:
160+
field_remapped_mid = (
161+
relevant_source_node_config.sdoc_to_source_map.get("MID", "MID")
162+
)
163+
164+
file_rewrites = {}
150165
for source_node_ in trace_info.source_nodes:
151166
function = source_node_.function
152167
if function is None or function.code_byte_range is None:
@@ -157,50 +172,101 @@ def _rewrite_source_file(
157172
if source_node_.comment_byte_range is None:
158173
continue
159174

160-
# FILE_PATH: The file the code resides in, relative to the root of the project repository.
161-
file_path = bytes(
162-
trace_info.source_file.in_doctree_source_file_rel_path_posix,
163-
encoding="utf8",
164-
)
175+
if field_remapped_mid not in source_node_.fields:
176+
continue
165177

166-
# INSTANCE: The requirement template instance, minus tags with hash strings.
167-
instance_bytes = bytearray()
168-
for field_name_, field_value_ in source_node_.fields.items():
169-
if field_name_ in ("SPDX-Req-ID", "SPDX-Req-HKey"):
170-
continue
171-
instance_bytes += bytes(field_value_, encoding="utf8")
172-
173-
# CODE: The code that the SPDX-Req applies to.
174-
code = file_bytes[
175-
function.code_byte_range.start : function.code_byte_range.end
176-
]
177-
178-
# This is important for Windows. Otherwise, the hash key will be calculated incorrectly.
179-
instance_bytes = instance_bytes.replace(b"\r\n", b"\n")
180-
code = code.replace(b"\r\n", b"\n")
181-
182-
hash_spdx_id = bytes(get_random_sha256(), encoding="utf8")
183-
hash_spdx_hash = generate_code_hash(
184-
project=bytes(project_config.project_title, encoding="utf8"),
185-
file_path=file_path,
186-
instance=bytes(instance_bytes),
187-
code=code,
188-
)
189-
patched_node = MarkerWriter().write(
190-
source_node_,
191-
rewrites={
192-
"SPDX-Req-ID": hash_spdx_id,
193-
"SPDX-Req-HKey": hash_spdx_hash,
194-
},
195-
comment_file_bytes=file_bytes[
196-
source_node_.comment_byte_range.start : source_node_.comment_byte_range.end
197-
],
198-
)
199-
rewrites[source_node_] = patched_node
178+
node_rewrites: Dict[str, bytes] = {}
179+
180+
# If the source node has the MID (SPDX-REQ-ID), but it is not yet a
181+
# valid SHA256 identifier, create one and patch the node.
182+
existing_req_id = source_node_.fields[field_remapped_mid]
183+
if not is_sha256(existing_req_id):
184+
hash_spdx_id_str = get_random_sha256()
185+
hash_spdx_id = bytes(hash_spdx_id_str, encoding="utf8")
186+
187+
if (sdoc_node_ := source_node_.sdoc_node) is not None:
188+
sdoc_node_.set_field_value(
189+
field_name="MID",
190+
form_field_index=0,
191+
value=hash_spdx_id_str,
192+
)
193+
node_rewrites[field_remapped_mid] = hash_spdx_id
194+
195+
patched_node = MarkerWriter().write(
196+
source_node_,
197+
rewrites=node_rewrites,
198+
comment_file_bytes=file_bytes[
199+
source_node_.comment_byte_range.start : source_node_.comment_byte_range.end
200+
],
201+
)
202+
file_rewrites[source_node_] = patched_node
203+
204+
# If a source node has no sidecar SDoc node attached, there is
205+
# nothing else to do.
206+
if source_node_.sdoc_node is None:
207+
continue
208+
209+
#
210+
# The following is only applicable to the Linux Kernel Requirements
211+
# Template proposal:
212+
#
213+
# Generate HASH field if it is not present. The HASH field is only
214+
# generated for SDoc nodes, the source code nodes are not modified.
215+
#
216+
sdoc_node: SDocNode = source_node_.sdoc_node
217+
218+
existing_req_hash: Optional[str] = None
219+
if "HASH" in sdoc_node.ordered_fields_lookup:
220+
hash_field = sdoc_node.get_field_by_name("HASH")
221+
existing_req_hash = hash_field.get_text_value()
222+
223+
if existing_req_hash is None or not is_sha256(existing_req_hash):
224+
# FILE_PATH: The file the code resides in, relative to the root of the project repository.
225+
file_path = bytes(
226+
trace_info.source_file.in_doctree_source_file_rel_path_posix,
227+
encoding="utf8",
228+
)
229+
230+
# INSTANCE: The requirement template instance, minus tags with hash strings.
231+
instance_bytes = bytearray()
232+
for (
233+
field_name_,
234+
field_values_,
235+
) in sdoc_node.ordered_fields_lookup.items():
236+
if field_name_ in ("MID", "HASH"):
237+
continue
238+
for field_value_ in field_values_:
239+
instance_bytes += bytes(
240+
field_value_.get_text_value(), encoding="utf8"
241+
)
242+
243+
# CODE: The code that the node hash applies to.
244+
code = file_bytes[
245+
function.code_byte_range.start : function.code_byte_range.end
246+
]
247+
248+
# This is important for Windows. Otherwise, the hash key will be calculated incorrectly.
249+
instance_bytes = instance_bytes.replace(b"\r\n", b"\n")
250+
code = code.replace(b"\r\n", b"\n")
251+
252+
hash_spdx_hash = generate_code_hash(
253+
project=bytes(
254+
project_config.project_title, encoding="utf8"
255+
),
256+
file_path=file_path,
257+
instance=bytes(instance_bytes),
258+
code=code,
259+
)
260+
hash_spdx_hash_str = hash_spdx_hash.decode("utf8")
261+
sdoc_node.set_field_value(
262+
field_name="HASH",
263+
form_field_index=0,
264+
value=hash_spdx_hash_str,
265+
)
200266

201267
source_writer = SourceWriter()
202268
output_string = source_writer.write(
203-
trace_info, rewrites=rewrites, file_bytes=file_bytes
269+
trace_info, rewrites=file_rewrites, file_bytes=file_bytes
204270
)
205271

206272
with open(trace_info.source_file.full_path, "wb") as source_file_:

0 commit comments

Comments
 (0)