模糊测试之书-十九-
模糊测试之书(十九)
原文:
exploringjs.com/ts/book/index.html译者:飞龙
计时器
本笔记本中的代码有助于测量时间。
先决条件
-
本笔记本需要一些对 Python 高级概念的理解,特别是
-
课程
-
Python 的
with语句 -
测量时间
-
概述
要使用本章提供的代码,请编写
>>> from fuzzingbook.Timer import <identifier>
然后利用以下功能。
Timer类允许您测量经过的实际时间(以分数秒为单位)。它的典型用法是与with子句结合:
>>> with Timer() as t:
>>> some_long_running_function()
>>> t.elapsed_time()
0.020704666967503726
测量时间
Timer类允许在代码执行期间测量经过的时间。
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils)
import [time](https://docs.python.org/3/library/time.html)
def clock() -> float:
"""
Return the number of fractional seconds elapsed since some point of reference.
"""
return time.perf_counter()
from [types](https://docs.python.org/3/library/types.html) import TracebackType
class Timer:
def __init__(self) -> None:
"""Constructor"""
self.start_time = clock()
self.end_time = None
def __enter__(self) -> Any:
"""Begin of `with` block"""
self.start_time = clock()
self.end_time = None
return self
def __exit__(self, exc_type: Type, exc_value: BaseException,
tb: TracebackType) -> None:
"""End of `with` block"""
self.end_time = clock()
def elapsed_time(self) -> float:
"""Return elapsed time in seconds"""
if self.end_time is None:
# still running
return clock() - self.start_time
else:
return self.end_time - self.start_time
这里有一个例子:
def some_long_running_function() -> None:
i = 1000000
while i > 0:
i -= 1
print("Stopping total time:")
with Timer() as t:
some_long_running_function()
print(t.elapsed_time())
Stopping total time:
0.020959541958291084
print("Stopping time in between:")
with Timer() as t:
for i in range(10):
some_long_running_function()
print(t.elapsed_time())
Stopping time in between:
0.01922783302143216
0.039810792019125074
0.06000966700958088
0.08149537502322346
0.10375195799861103
0.12619870802154765
0.14785908302292228
0.16952008299995214
0.19183695799438283
0.21232758299447596
好了,各位——享受吧!
经验教训
- 使用
Timer类,测量经过的时间非常容易。
本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受MIT 许可协议许可。 最后更改:2023-11-11 18:25:47+01:00 • 引用 • 版权信息
如何引用本作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "Timer". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "The Fuzzing Book", www.fuzzingbook.org/html/Timer.html. Retrieved 2023-11-11 18:25:47+01:00.
@incollection{fuzzingbook2023:Timer,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Timer},
year = {2023},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/Timer.html}},
note = {Retrieved 2023-11-11 18:25:47+01:00},
url = {https://www.fuzzingbook.org/html/Timer.html},
urldate = {2023-11-11 18:25:47+01:00}
}
超时
本笔记本中的代码有助于在给定时间后中断执行。
先决条件
-
这个笔记本需要对 Python 的高级概念有所了解,特别是
-
类
-
Python 的
with语句 -
Python 的
signal函数 -
测量时间
-
概述
要 使用本章节提供的代码,请编写
>>> from fuzzingbook.Timeout import <identifier>
然后利用以下功能。
Timeout 类在给定超时时间到期后抛出 TimeoutError 异常。它的典型用法是与 with 语句结合使用:
>>> try:
>>> with Timeout(0.2):
>>> some_long_running_function()
>>> print("complete!")
>>> except TimeoutError:
>>> print("Timeout!")
Timeout!
注意:在 Unix/Linux 系统上,Timeout 类使用 SIGALRM 信号(中断)来实现超时;这对跟踪代码的性能没有影响。在其他系统(特别是 Windows)上,Timeout 使用 sys.settrace() 函数在每行代码后检查计时器,这会影响跟踪代码的性能。
测量时间
Timeout 类允许在给定时间间隔后中断某些代码执行。
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils)
import [time](https://docs.python.org/3/library/time.html)
from [types](https://docs.python.org/3/library/types.html) import FrameType, TracebackType
变体 1:Unix(使用信号,高效)
import [signal](https://docs.python.org/3/library/signal.html)
class SignalTimeout:
"""Execute a code block raising a timeout."""
def __init__(self, timeout: Union[int, float]) -> None:
"""
Constructor. Interrupt execution after `timeout` seconds.
"""
self.timeout = timeout
self.old_handler: Any = signal.SIG_DFL
self.old_timeout = 0.0
def __enter__(self) -> Any:
"""Begin of `with` block"""
# Register timeout() as handler for signal 'SIGALRM'"
self.old_handler = signal.signal(signal.SIGALRM, self.timeout_handler)
self.old_timeout, _ = signal.setitimer(signal.ITIMER_REAL, self.timeout)
return self
def __exit__(self, exc_type: Type, exc_value: BaseException,
tb: TracebackType) -> None:
"""End of `with` block"""
self.cancel()
return # re-raise exception, if any
def cancel(self) -> None:
"""Cancel timeout"""
signal.signal(signal.SIGALRM, self.old_handler)
signal.setitimer(signal.ITIMER_REAL, self.old_timeout)
def timeout_handler(self, signum: int, frame: Optional[FrameType]) -> None:
"""Handle timeout (SIGALRM) signal"""
raise TimeoutError()
这里是一个例子:
def some_long_running_function() -> None:
i = 10000000
while i > 0:
i -= 1
try:
with SignalTimeout(0.2):
some_long_running_function()
print("Complete!")
except TimeoutError:
print("Timeout!")
Timeout!
变体 2:通用/Windows(使用跟踪,效率不高)
import [sys](https://docs.python.org/3/library/sys.html)
class GenericTimeout:
"""Execute a code block raising a timeout."""
def __init__(self, timeout: Union[int, float]) -> None:
"""
Constructor. Interrupt execution after `timeout` seconds.
"""
self.seconds_before_timeout = timeout
self.original_trace_function: Optional[Callable] = None
self.end_time: Optional[float] = None
def check_time(self, frame: FrameType, event: str, arg: Any) -> Callable:
"""Tracing function"""
if self.original_trace_function is not None:
self.original_trace_function(frame, event, arg)
current_time = time.time()
if self.end_time and current_time >= self.end_time:
raise TimeoutError
return self.check_time
def __enter__(self) -> Any:
"""Begin of `with` block"""
start_time = time.time()
self.end_time = start_time + self.seconds_before_timeout
self.original_trace_function = sys.gettrace()
sys.settrace(self.check_time)
return self
def __exit__(self, exc_type: type,
exc_value: BaseException, tb: TracebackType) -> Optional[bool]:
"""End of `with` block"""
self.cancel()
return None # re-raise exception, if any
def cancel(self) -> None:
"""Cancel timeout"""
sys.settrace(self.original_trace_function)
再次,我们的例子:
try:
with GenericTimeout(0.2):
some_long_running_function()
print("Complete!")
except TimeoutError:
print("Timeout!")
Timeout!
选择正确的变体
Timeout: Type[SignalTimeout] = SignalTimeout if hasattr(signal, 'SIGALRM') else GenericTimeout
练习
创建一个在 Windows 上高效运行的 Timeout 变体。请注意,如何在编程论坛上这是一个长期争论的问题。
本项目的内容受 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受 MIT 许可协议 的许可。 最后更改:2023-11-11 18:25:46+01:00 • 引用 • 版权信息
如何引用本作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "Timeout". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "The Fuzzing Book", www.fuzzingbook.org/html/Timeout.html. Retrieved 2023-11-11 18:25:46+01:00.
@incollection{fuzzingbook2023:Timeout,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Timeout},
year = {2023},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/Timeout.html}},
note = {Retrieved 2023-11-11 18:25:46+01:00},
url = {https://www.fuzzingbook.org/html/Timeout.html},
urldate = {2023-11-11 18:25:46+01:00}
}
类图
这是一个简单的类图查看器。针对书籍进行了定制。
先决条件
- 在此处参考早期章节作为笔记本,如下所示: 早期章节。
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils)
概述
要使用本章提供的代码(Importing.html),请编写
>>> from fuzzingbook.ClassDiagram import <identifier>
然后利用以下功能。
函数 display_class_hierarchy() 显示给定类(或类列表)的类层次结构。
-
关键字参数
public_methods,如果提供,是一个要由客户端使用的“公共”方法列表(默认:所有带有文档字符串的方法)。 -
关键字参数
abstract_classes,如果提供,是一个要显示为“抽象”的类列表(即带有草书类名)。
>>> display_class_hierarchy(D_Class, abstract_classes=[A_Class])
从多个超类继承的子类。
附带相当长但无意义的文档。">
一架二战时期的 foo 战斗机。">
继承了一些方法的子类。">
一个 qux 走进了一家酒吧。
bartender 是一个可选属性。">
一架二战时期的 foo 战斗机。">
一个注入某些方法的类">
一个正确完成 A 任务的类。
附带较长的文档字符串。">
《辉煌的 Foo 的冒险》">
一个未被使用的。">
获取类层次结构
import [inspect](https://docs.python.org/3/library/inspect.html)
使用 mro(),我们可以访问类层次结构。我们确保避免由 class X(X) 创建的重复项。
def class_hierarchy(cls: Type) -> List[Type]:
superclasses = cls.mro()
hierarchy = []
last_superclass_name = ""
for superclass in superclasses:
if superclass.__name__ != last_superclass_name:
hierarchy.append(superclass)
last_superclass_name = superclass.__name__
return hierarchy
这里有一个例子:
class A_Class:
"""A Class which does A thing right.
Comes with a longer docstring."""
def foo(self) -> None:
"""The Adventures of the glorious Foo"""
pass
def quux(self) -> None:
"""A method that is not used."""
pass
class A_Class(A_Class):
# We define another function in a separate cell.
def second(self) -> None:
pass
class B_Class(A_Class):
"""A subclass inheriting some methods."""
VAR = "A variable"
def foo(self) -> None:
"""A WW2 foo fighter."""
pass
def bar(self, qux: Any = None, bartender: int = 42) -> None:
"""A qux walks into a bar.
`bartender` is an optional attribute."""
pass
SomeType = List[Optional[Union[str, int]]]
class C_Class:
"""A class injecting some method"""
def qux(self, arg: SomeType) -> SomeType:
return arg
class D_Class(B_Class, C_Class):
"""A subclass inheriting from multiple superclasses.
Comes with a fairly long, but meaningless documentation."""
def foo(self) -> None:
B_Class.foo(self)
class D_Class(D_Class):
pass # An incremental addiiton that should not impact D's semantics
class_hierarchy(D_Class)
[__main__.D_Class,
__main__.B_Class,
__main__.A_Class,
__main__.C_Class,
object]
获取类树
我们可以使用 __bases__ 来获取直接基类。
D_Class.__bases__
(__main__.D_Class,)
class_tree() 返回一个类树,使用具有相同名称的“最低”(最专用)的类。
def class_tree(cls: Type, lowest: Optional[Type] = None) -> List[Tuple[Type, List]]:
ret = []
for base in cls.__bases__:
if base.__name__ == cls.__name__:
if not lowest:
lowest = cls
ret += class_tree(base, lowest)
else:
if lowest:
cls = lowest
ret.append((cls, class_tree(base)))
return ret
class_tree(D_Class)
[(__main__.D_Class, [(__main__.B_Class, [(__main__.A_Class, [])])]),
(__main__.D_Class, [(__main__.C_Class, [])])]
class_tree(D_Class)[0][0]
__main__.D_Class
assert class_tree(D_Class)[0][0] == D_Class
class_set() 将树扁平化为集合:
def class_set(classes: Union[Type, List[Type]]) -> Set[Type]:
if not isinstance(classes, list):
classes = [classes]
ret = set()
def traverse_tree(tree: List[Tuple[Type, List]]) -> None:
for (cls, subtrees) in tree:
ret.add(cls)
for subtree in subtrees:
traverse_tree(subtrees)
for cls in classes:
traverse_tree(class_tree(cls))
return ret
class_set(D_Class)
{__main__.A_Class, __main__.B_Class, __main__.C_Class, __main__.D_Class}
assert A_Class in class_set(D_Class)
assert B_Class in class_set(D_Class)
assert C_Class in class_set(D_Class)
assert D_Class in class_set(D_Class)
class_set([B_Class, C_Class])
{__main__.A_Class, __main__.B_Class, __main__.C_Class}
获取文档
A_Class.__doc__
A_Class.__bases__[0].__doc__
'A Class which does A thing right.\n Comes with a longer docstring.'
A_Class.__bases__[0].__name__
'A_Class'
D_Class.foo
<function __main__.D_Class.foo(self) -> None>
D_Class.foo.__doc__
A_Class.foo.__doc__
'The Adventures of the glorious Foo'
def docstring(obj: Any) -> str:
doc = inspect.getdoc(obj)
return doc if doc else ""
docstring(A_Class)
'A Class which does A thing right.\nComes with a longer docstring.'
docstring(D_Class.foo)
'A WW2 foo fighter.'
def unknown() -> None:
pass
docstring(unknown)
''
import [html](https://docs.python.org/3/library/html.html)
import [re](https://docs.python.org/3/library/re.html)
def escape(text: str) -> str:
text = html.escape(text)
assert '<' not in text
assert '>' not in text
text = text.replace('{', '{')
text = text.replace('|', '|')
text = text.replace('}', '}')
return text
escape("f(foo={})")
'f(foo={})'
def escape_doc(docstring: str) -> str:
DOC_INDENT = 0
docstring = "
".join(
' ' * DOC_INDENT + escape(line).strip()
for line in docstring.split('\n')
)
return docstring
print(escape_doc("'Hello\n {You|Me}'"))
'Hello
{You|Me}'
获取方法和变量
inspect.getmembers(D_Class)
[('VAR', 'A variable'),
('__class__', type),
('__delattr__', <slot wrapper '__delattr__' of 'object' objects>),
('__dict__', mappingproxy({'__module__': '__main__', '__doc__': None})),
('__dir__', <method '__dir__' of 'object' objects>),
('__doc__', None),
('__eq__', <slot wrapper '__eq__' of 'object' objects>),
('__format__', <method '__format__' of 'object' objects>),
('__ge__', <slot wrapper '__ge__' of 'object' objects>),
('__getattribute__', <slot wrapper '__getattribute__' of 'object' objects>),
('__getstate__', <method '__getstate__' of 'object' objects>),
('__gt__', <slot wrapper '__gt__' of 'object' objects>),
('__hash__', <slot wrapper '__hash__' of 'object' objects>),
('__init__', <slot wrapper '__init__' of 'object' objects>),
('__init_subclass__', <function D_Class.__init_subclass__>),
('__le__', <slot wrapper '__le__' of 'object' objects>),
('__lt__', <slot wrapper '__lt__' of 'object' objects>),
('__module__', '__main__'),
('__ne__', <slot wrapper '__ne__' of 'object' objects>),
('__new__', <function object.__new__(*args, **kwargs)>),
('__reduce__', <method '__reduce__' of 'object' objects>),
('__reduce_ex__', <method '__reduce_ex__' of 'object' objects>),
('__repr__', <slot wrapper '__repr__' of 'object' objects>),
('__setattr__', <slot wrapper '__setattr__' of 'object' objects>),
('__sizeof__', <method '__sizeof__' of 'object' objects>),
('__str__', <slot wrapper '__str__' of 'object' objects>),
('__subclasshook__', <function D_Class.__subclasshook__>),
('__weakref__', <attribute '__weakref__' of 'A_Class' objects>),
('bar',
<function __main__.B_Class.bar(self, qux: Any = None, bartender: int = 42) -> None>),
('foo', <function __main__.D_Class.foo(self) -> None>),
('quux', <function __main__.A_Class.quux(self) -> None>),
('qux',
<function __main__.C_Class.qux(self, arg: List[Union[int, str, NoneType]]) -> List[Union[int, str, NoneType]]>),
('second', <function __main__.A_Class.second(self) -> None>)]
def class_items(cls: Type, pred: Callable) -> List[Tuple[str, Any]]:
def _class_items(cls: Type) -> List:
all_items = inspect.getmembers(cls, pred)
for base in cls.__bases__:
all_items += _class_items(base)
return all_items
unique_items = []
items_seen = set()
for (name, item) in _class_items(cls):
if name not in items_seen:
unique_items.append((name, item))
items_seen.add(name)
return unique_items
def class_methods(cls: Type) -> List[Tuple[str, Callable]]:
return class_items(cls, inspect.isfunction)
def defined_in(name: str, cls: Type) -> bool:
if not hasattr(cls, name):
return False
defining_classes = []
def search_superclasses(name: str, cls: Type) -> None:
if not hasattr(cls, name):
return
for base in cls.__bases__:
if hasattr(base, name):
defining_classes.append(base)
search_superclasses(name, base)
search_superclasses(name, cls)
if any(cls.__name__ != c.__name__ for c in defining_classes):
return False # Already defined in superclass
return True
assert not defined_in('VAR', A_Class)
assert defined_in('VAR', B_Class)
assert not defined_in('VAR', C_Class)
assert not defined_in('VAR', D_Class)
def class_vars(cls: Type) -> List[Any]:
def is_var(item: Any) -> bool:
return not callable(item)
return [item for item in class_items(cls, is_var)
if not item[0].startswith('__') and defined_in(item[0], cls)]
class_methods(D_Class)
[('bar',
<function __main__.B_Class.bar(self, qux: Any = None, bartender: int = 42) -> None>),
('foo', <function __main__.D_Class.foo(self) -> None>),
('quux', <function __main__.A_Class.quux(self) -> None>),
('qux',
<function __main__.C_Class.qux(self, arg: List[Union[int, str, NoneType]]) -> List[Union[int, str, NoneType]]>),
('second', <function __main__.A_Class.second(self) -> None>)]
class_vars(B_Class)
[('VAR', 'A variable')]
我们只对
-
在该类中定义的函数
-
带有文档字符串的函数
def public_class_methods(cls: Type) -> List[Tuple[str, Callable]]:
return [(name, method) for (name, method) in class_methods(cls)
if method.__qualname__.startswith(cls.__name__)]
def doc_class_methods(cls: Type) -> List[Tuple[str, Callable]]:
return [(name, method) for (name, method) in public_class_methods(cls)
if docstring(method) is not None]
public_class_methods(D_Class)
[('foo', <function __main__.D_Class.foo(self) -> None>)]
doc_class_methods(D_Class)
[('foo', <function __main__.D_Class.foo(self) -> None>)]
def overloaded_class_methods(classes: Union[Type, List[Type]]) -> Set[str]:
all_methods: Dict[str, Set[Callable]] = {}
for cls in class_set(classes):
for (name, method) in class_methods(cls):
if method.__qualname__.startswith(cls.__name__):
all_methods.setdefault(name, set())
all_methods[name].add(cls)
return set(name for name in all_methods if len(all_methods[name]) >= 2)
overloaded_class_methods(D_Class)
{'foo'}
使用方法名称绘制类层次结构
from [inspect](https://docs.python.org/3/library/inspect.html) import signature
import [warnings](https://docs.python.org/3/library/warnings.html)
import [os](https://docs.python.org/3/library/os.html)
def display_class_hierarchy(classes: Union[Type, List[Type]], *,
public_methods: Optional[List] = None,
abstract_classes: Optional[List] = None,
include_methods: bool = True,
include_class_vars: bool = True,
include_legend: bool = True,
local_defs_only: bool = True,
types: Dict[str, Any] = {},
project: str = 'fuzzingbook',
log: bool = False) -> Any:
"""Visualize a class hierarchy.
`classes` is a Python class (or a list of classes) to be visualized.
`public_methods`, if given, is a list of methods to be shown as "public" (bold).
(Default: all methods with a docstring)
`abstract_classes`, if given, is a list of classes to be shown as "abstract" (cursive).
(Default: all classes with an abstract method)
`include_methods`: if set (default), include all methods
`include_legend`: if set (default), include a legend
`local_defs_only`: if set (default), hide details of imported classes
`types`: type names with definitions, to be used in docs
"""
from [graphviz](https://graphviz.readthedocs.io/) import Digraph
if project == 'debuggingbook':
CLASS_FONT = 'Raleway, Helvetica, Arial, sans-serif'
CLASS_COLOR = '#6A0DAD' # HTML 'purple'
else:
CLASS_FONT = 'Patua One, Helvetica, sans-serif'
CLASS_COLOR = '#B03A2E'
METHOD_FONT = "'Fira Mono', 'Source Code Pro', 'Courier', monospace"
METHOD_COLOR = 'black'
if isinstance(classes, list):
starting_class = classes[0]
else:
starting_class = classes
classes = [starting_class]
title = starting_class.__name__ + " class hierarchy"
dot = Digraph(comment=title)
dot.attr('node', shape='record', fontname=CLASS_FONT)
dot.attr('graph', rankdir='BT', tooltip=title)
dot.attr('edge', arrowhead='empty')
# Hack to force rendering as HTML, allowing hovers and links in Jupyter
dot._repr_html_ = dot._repr_image_svg_xml
edges = set()
overloaded_methods: Set[str] = set()
drawn_classes = set()
def method_string(method_name: str, public: bool, overloaded: bool,
fontsize: float = 10.0) -> str:
method_string = f'<font face="{METHOD_FONT}" point-size="{str(fontsize)}">'
if overloaded:
name = f'<i>{method_name}()</i>'
else:
name = f'{method_name}()'
if public:
method_string += f'<b>{name}</b>'
else:
method_string += f'<font color="{METHOD_COLOR}">' \
f'{name}</font>'
method_string += '</font>'
return method_string
def var_string(var_name: str, fontsize: int = 10) -> str:
var_string = f'<font face="{METHOD_FONT}" point-size="{str(fontsize)}">'
var_string += f'{var_name}'
var_string += '</font>'
return var_string
def is_overloaded(method_name: str, f: Any) -> bool:
return (method_name in overloaded_methods or
(docstring(f) is not None and "in subclasses" in docstring(f)))
def is_abstract(cls: Type) -> bool:
if not abstract_classes:
return inspect.isabstract(cls)
return (cls in abstract_classes or
any(c.__name__ == cls.__name__ for c in abstract_classes))
def is_public(method_name: str, f: Any) -> bool:
if public_methods:
return (method_name in public_methods or
f in public_methods or
any(f.__qualname__ == m.__qualname__
for m in public_methods))
return bool(docstring(f))
def frame_module(frameinfo: Any) -> str:
return os.path.splitext(os.path.basename(frameinfo.frame.f_code.co_filename))[0]
def callers() -> List[str]:
frames = inspect.getouterframes(inspect.currentframe())
return [frame_module(frameinfo) for frameinfo in frames]
def is_local_class(cls: Type) -> bool:
return cls.__module__ == '__main__' or cls.__module__ in callers()
def class_vars_string(cls: Type, url: str) -> str:
cls_vars = class_vars(cls)
if len(cls_vars) == 0:
return ""
vars_string = f'<table border="0" cellpadding="0" ' \
f'cellspacing="0" ' \
f'align="left" tooltip="{cls.__name__}" href="#">'
for (name, var) in cls_vars:
if log:
print(f" Drawing {name}")
var_doc = escape(f"{name} = {repr(var)}")
tooltip = f' tooltip="{var_doc}"'
href = f' href="{url}"'
vars_string += f'<tr><td align="left" border="0"' \
f'{tooltip}{href}>'
vars_string += var_string(name)
vars_string += '</td></tr>'
vars_string += '</table>'
return vars_string
def class_methods_string(cls: Type, url: str) -> str:
methods = public_class_methods(cls)
# return "<br/>".join([name + "()" for (name, f) in methods])
methods_string = f'<table border="0" cellpadding="0" ' \
f'cellspacing="0" ' \
f'align="left" tooltip="{cls.__name__}" href="#">'
public_methods_only = local_defs_only and not is_local_class(cls)
methods_seen = False
for public in [True, False]:
for (name, f) in methods:
if public != is_public(name, f):
continue
if public_methods_only and not public:
continue
if log:
print(f" Drawing {name}()")
if is_public(name, f) and not docstring(f):
warnings.warn(f"{f.__qualname__}() is listed as public,"
f" but has no docstring")
overloaded = is_overloaded(name, f)
sig = str(inspect.signature(f))
# replace 'List[Union[...]]' by the actual type def
for tp in types:
tp_def = str(types[tp]).replace('typing.', '')
sig = sig.replace(tp_def, tp)
sig = sig.replace('__main__.', '')
method_doc = escape(name + sig)
if docstring(f):
method_doc += ":
" + escape_doc(docstring(f))
if log:
print(f" Method doc: {method_doc}")
# Tooltips are only shown if a href is present, too
tooltip = f' tooltip="{method_doc}"'
href = f' href="{url}"'
methods_string += f'<tr><td align="left" border="0"' \
f'{tooltip}{href}>'
methods_string += method_string(name, public, overloaded)
methods_string += '</td></tr>'
methods_seen = True
if not methods_seen:
return ""
methods_string += '</table>'
return methods_string
def display_class_node(cls: Type) -> None:
name = cls.__name__
if name in drawn_classes:
return
drawn_classes.add(name)
if log:
print(f"Drawing class {name}")
if cls.__module__ == '__main__':
url = '#'
else:
url = cls.__module__ + '.ipynb'
if is_abstract(cls):
formatted_class_name = f'<i>{cls.__name__}</i>'
else:
formatted_class_name = cls.__name__
if include_methods or include_class_vars:
vars = class_vars_string(cls, url)
methods = class_methods_string(cls, url)
spec = '<{<b><font color="' + CLASS_COLOR + '">' + \
formatted_class_name + '</font></b>'
if include_class_vars and vars:
spec += '|' + vars
if include_methods and methods:
spec += '|' + methods
spec += '}>'
else:
spec = '<' + formatted_class_name + '>'
class_doc = escape('class ' + cls.__name__)
if docstring(cls):
class_doc += ':
' + escape_doc(docstring(cls))
else:
warnings.warn(f"Class {cls.__name__} has no docstring")
dot.node(name, spec, tooltip=class_doc, href=url)
def display_class_trees(trees: List[Tuple[Type, List]]) -> None:
for tree in trees:
(cls, subtrees) = tree
display_class_node(cls)
for subtree in subtrees:
(subcls, _) = subtree
if (cls.__name__, subcls.__name__) not in edges:
dot.edge(cls.__name__, subcls.__name__)
edges.add((cls.__name__, subcls.__name__))
display_class_trees(subtrees)
def display_legend() -> None:
fontsize = 8.0
label = f'<b><font color="{CLASS_COLOR}">Legend</font></b><br align="left"/>'
for item in [
method_string("public_method",
public=True, overloaded=False, fontsize=fontsize),
method_string("private_method",
public=False, overloaded=False, fontsize=fontsize),
method_string("overloaded_method",
public=False, overloaded=True, fontsize=fontsize)
]:
label += '• ' + item + '<br align="left"/>'
label += f'<font face="Helvetica" point-size="{str(fontsize + 1)}">' \
'Hover over names to see doc' \
'</font><br align="left"/>'
dot.node('Legend', label=f'<{label}>', shape='plain', fontsize=str(fontsize + 2))
for cls in classes:
tree = class_tree(cls)
overloaded_methods = overloaded_class_methods(cls)
display_class_trees(tree)
if include_legend:
display_legend()
return dot
display_class_hierarchy(D_Class, types={'SomeType': SomeType},
project='debuggingbook', log=True)
Drawing class D_Class
Drawing foo()
Method doc: foo(self) -> None:
A WW2 foo fighter.
Drawing class B_Class
Drawing VAR
Drawing bar()
Method doc: bar(self, qux: Any = None, bartender: int = 42) -> None:
A qux walks into a bar.
`bartender` is an optional attribute.
Drawing foo()
Method doc: foo(self) -> None:
A WW2 foo fighter.
Drawing class A_Class
Drawing foo()
Method doc: foo(self) -> None:
The Adventures of the glorious Foo
Drawing quux()
Method doc: quux(self) -> None:
A method that is not used.
Drawing second()
Method doc: second(self) -> None
Drawing class C_Class
Drawing qux()
Method doc: qux(self, arg: SomeType) -> SomeType
从多个超类继承的子类。
伴随相当长但无意义的文档。">
一架二战时期的 foo 战斗机。">
继承了一些方法的子类。《B_Class》
一个 qux 走进酒吧。
bartender是一个可选属性。">
一架二战时期的 foo 战斗机。">
注入了一些方法的类。《C_Class》
一个正确完成 A 任务的类。
带有更长的文档字符串。">
The Adventures of the glorious Foo">
未使用的方法。">
display_class_hierarchy(D_Class, types={'SomeType': SomeType},
project='fuzzingbook')
A subclass inheriting from multiple superclasses.
Comes with a fairly long, but meaningless documentation.">
一架二战时期的 foo 战斗机。">
继承一些方法的子类。《B_Class
bartender 是一个可选属性。">
一架二战时期的 foo 战斗机。">
向类中注入一些方法。《C_Class
正确完成 A 任务的类。
带有更长的文档字符串。">
荣耀的 Foo 的冒险故事">
未使用的方 法。">
这里是一个带有抽象类和日志记录的变体:
display_class_hierarchy([A_Class, B_Class],
abstract_classes=[A_Class],
public_methods=[
A_Class.quux,
],
log=True)
Drawing class A_Class
Drawing quux()
Method doc: quux(self) -> None:
A method that is not used.
Drawing foo()
Method doc: foo(self) -> None:
The Adventures of the glorious Foo
Drawing second()
Method doc: second(self) -> None
Drawing class B_Class
Drawing VAR
Drawing bar()
Method doc: bar(self, qux: Any = None, bartender: int = 42) -> None:
A qux walks into a bar.
`bartender` is an optional attribute.
Drawing foo()
Method doc: foo(self) -> None:
A WW2 foo fighter.
正确执行某事的类。
附带更长的文档字符串。">
一个未使用的函数。">
光荣的 Foo 的冒险故事">
继承了一些方法的子类。">
一个 qux 走进了一家酒吧。
bartender是一个可选属性。">
一架二战时期的 foo 战斗机。">
练习
享受阅读!
本项目的内容受 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码受 MIT 许可协议 的许可。 最后更改:2024-06-30 18:45:02+02:00 • 引用 • 版权信息
如何引用这篇作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "类图". 在 Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler 编著的 "模糊测试书籍", www.fuzzingbook.org/html/ClassDiagram.html. Retrieved 2024-06-30 18:45:02+02:00.
@incollection{fuzzingbook2024:ClassDiagram,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Class Diagrams},
year = {2024},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/ClassDiagram.html}},
note = {Retrieved 2024-06-30 18:45:02+02:00},
url = {https://www.fuzzingbook.org/html/ClassDiagram.html},
urldate = {2024-06-30 18:45:02+02:00}
}
铁路图
这个笔记本中的代码有助于绘制语法图。它是 Tab Atkins jr. 的优秀库(略有定制)的副本,遗憾的是,这个库不是一个 Python 包。
先决条件
这个笔记本需要一些对 Python 和图形的高级概念的理解,特别是
* classes
* the Python `with` statement
* Scalable Vector Graphics
铁路图实现
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils)
import [re](https://docs.python.org/3/library/re.html)
import [io](https://docs.python.org/3/library/io.html)
class C:
# Display constants
DEBUG = False # if true, writes some debug information into attributes
VS = 8 # minimum vertical separation between things. For a 3px stroke, must be at least 4
AR = 10 # radius of arcs
DIAGRAM_CLASS = 'railroad-diagram' # class to put on the root <svg>
# is the stroke width an odd (1px, 3px, etc) pixel length?
STROKE_ODD_PIXEL_LENGTH = True
# how to align items when they have extra space. left/right/center
INTERNAL_ALIGNMENT = 'center'
# width of each monospace character. play until you find the right value
# for your font
CHAR_WIDTH = 8.5
COMMENT_CHAR_WIDTH = 7 # comments are in smaller text by default
DEFAULT_STYLE = '''\
svg.railroad-diagram {
}
svg.railroad-diagram path {
stroke-width:3;
stroke:black;
fill:white;
}
svg.railroad-diagram text {
font:14px "Fira Mono", monospace;
text-anchor:middle;
}
svg.railroad-diagram text.label{
text-anchor:start;
}
svg.railroad-diagram text.comment{
font:italic 12px "Fira Mono", monospace;
}
svg.railroad-diagram rect{
stroke-width:2;
stroke:black;
fill:mistyrose;
}
'''
def e(text):
text = re.sub(r"&", '&', str(text))
text = re.sub(r"<", '<', str(text))
text = re.sub(r">", '>', str(text))
return str(text)
def determineGaps(outer, inner):
diff = outer - inner
if C.INTERNAL_ALIGNMENT == 'left':
return 0, diff
elif C.INTERNAL_ALIGNMENT == 'right':
return diff, 0
else:
return diff / 2, diff / 2
def doubleenumerate(seq):
length = len(list(seq))
for i, item in enumerate(seq):
yield i, i - length, item
def addDebug(el):
if not C.DEBUG:
return
el.attrs['data-x'] = "{0} w:{1} h:{2}/{3}/{4}".format(
type(el).__name__, el.width, el.up, el.height, el.down)
class DiagramItem:
def __init__(self, name, attrs=None, text=None):
self.name = name
# up = distance it projects above the entry line
# height = distance between the entry/exit lines
# down = distance it projects below the exit line
self.height = 0
self.attrs = attrs or {}
self.children = [text] if text else []
self.needsSpace = False
def format(self, x, y, width):
raise NotImplementedError # Virtual
def addTo(self, parent):
parent.children.append(self)
return self
def writeSvg(self, write):
write(u'<{0}'.format(self.name))
for name, value in sorted(self.attrs.items()):
write(u' {0}="{1}"'.format(name, e(value)))
write(u'>')
if self.name in ["g", "svg"]:
write(u'\n')
for child in self.children:
if isinstance(child, DiagramItem):
child.writeSvg(write)
else:
write(e(child))
write(u'</{0}>'.format(self.name))
def __eq__(self, other):
return isinstance(self, type(
other)) and self.__dict__ == other.__dict__
def __ne__(self, other):
return not (self == other)
class Path(DiagramItem):
def __init__(self, x, y):
self.x = x
self.y = y
DiagramItem.__init__(self, 'path', {'d': 'M%s %s' % (x, y)})
def m(self, x, y):
self.attrs['d'] += 'm{0} {1}'.format(x, y)
return self
def ll(self, x, y): # was l(), which violates PEP8 -- AZ
self.attrs['d'] += 'l{0} {1}'.format(x, y)
return self
def h(self, val):
self.attrs['d'] += 'h{0}'.format(val)
return self
def right(self, val):
return self.h(max(0, val))
def left(self, val):
return self.h(-max(0, val))
def v(self, val):
self.attrs['d'] += 'v{0}'.format(val)
return self
def down(self, val):
return self.v(max(0, val))
def up(self, val):
return self.v(-max(0, val))
def arc_8(self, start, dir):
# 1/8 of a circle
arc = C.AR
s2 = 1 / math.sqrt(2) * arc
s2inv = (arc - s2)
path = "a {0} {0} 0 0 {1} ".format(arc, "1" if dir == 'cw' else "0")
sd = start + dir
if sd == 'ncw':
offset = [s2, s2inv]
elif sd == 'necw':
offset = [s2inv, s2]
elif sd == 'ecw':
offset = [-s2inv, s2]
elif sd == 'secw':
offset = [-s2, s2inv]
elif sd == 'scw':
offset = [-s2, -s2inv]
elif sd == 'swcw':
offset = [-s2inv, -s2]
elif sd == 'wcw':
offset = [s2inv, -s2]
elif sd == 'nwcw':
offset = [s2, -s2inv]
elif sd == 'nccw':
offset = [-s2, s2inv]
elif sd == 'nwccw':
offset = [-s2inv, s2]
elif sd == 'wccw':
offset = [s2inv, s2]
elif sd == 'swccw':
offset = [s2, s2inv]
elif sd == 'sccw':
offset = [s2, -s2inv]
elif sd == 'seccw':
offset = [s2inv, -s2]
elif sd == 'eccw':
offset = [-s2inv, -s2]
elif sd == 'neccw':
offset = [-s2, -s2inv]
path += " ".join(str(x) for x in offset)
self.attrs['d'] += path
return self
def arc(self, sweep):
x = C.AR
y = C.AR
if sweep[0] == 'e' or sweep[1] == 'w':
x *= -1
if sweep[0] == 's' or sweep[1] == 'n':
y *= -1
cw = 1 if sweep == 'ne' or sweep == 'es' or sweep == 'sw' or sweep == 'wn' else 0
self.attrs['d'] += 'a{0} {0} 0 0 {1} {2} {3}'.format(C.AR, cw, x, y)
return self
def format(self):
self.attrs['d'] += 'h.5'
return self
def __repr__(self):
return 'Path(%r, %r)' % (self.x, self.y)
def wrapString(value):
return value if isinstance(value, DiagramItem) else Terminal(value)
class Style(DiagramItem):
def __init__(self, css):
self.name = 'style'
self.css = css
self.height = 0
self.width = 0
self.needsSpace = False
def __repr__(self):
return 'Style(%r)' % css
def format(self, x, y, width):
return self
def writeSvg(self, write):
# Write included stylesheet as CDATA. See
# https:#developer.mozilla.org/en-US/docs/Web/SVG/Element/style
cdata = u'/* <![CDATA[ */\n{css}\n/* ]]> */\n'.format(css=self.css)
write(u'<style>{cdata}</style>'.format(cdata=cdata))
class Diagram(DiagramItem):
def __init__(self, *items, **kwargs):
# Accepts a type=[simple|complex] kwarg
DiagramItem.__init__(
self, 'svg', {'class': C.DIAGRAM_CLASS, 'xmlns': "http://www.w3.org/2000/svg"})
self.type = kwargs.get("type", "simple")
self.items = [wrapString(item) for item in items]
if items and not isinstance(items[0], Start):
self.items.insert(0, Start(self.type))
if items and not isinstance(items[-1], End):
self.items.append(End(self.type))
self.css = kwargs.get("css", C.DEFAULT_STYLE)
if self.css:
self.items.insert(0, Style(self.css))
self.up = 0
self.down = 0
self.height = 0
self.width = 0
for item in self.items:
if isinstance(item, Style):
continue
self.width += item.width + (20 if item.needsSpace else 0)
self.up = max(self.up, item.up - self.height)
self.height += item.height
self.down = max(self.down - item.height, item.down)
if self.items[0].needsSpace:
self.width -= 10
if self.items[-1].needsSpace:
self.width -= 10
self.formatted = False
def __repr__(self):
if self.css:
items = ', '.join(map(repr, self.items[2:-1]))
else:
items = ', '.join(map(repr, self.items[1:-1]))
pieces = [] if not items else [items]
if self.css != C.DEFAULT_STYLE:
pieces.append('css=%r' % self.css)
if self.type != 'simple':
pieces.append('type=%r' % self.type)
return 'Diagram(%s)' % ', '.join(pieces)
def format(self, paddingTop=20, paddingRight=None,
paddingBottom=None, paddingLeft=None):
if paddingRight is None:
paddingRight = paddingTop
if paddingBottom is None:
paddingBottom = paddingTop
if paddingLeft is None:
paddingLeft = paddingRight
x = paddingLeft
y = paddingTop + self.up
g = DiagramItem('g')
if C.STROKE_ODD_PIXEL_LENGTH:
g.attrs['transform'] = 'translate(.5 .5)'
for item in self.items:
if item.needsSpace:
Path(x, y).h(10).addTo(g)
x += 10
item.format(x, y, item.width).addTo(g)
x += item.width
y += item.height
if item.needsSpace:
Path(x, y).h(10).addTo(g)
x += 10
self.attrs['width'] = self.width + paddingLeft + paddingRight
self.attrs['height'] = self.up + self.height + \
self.down + paddingTop + paddingBottom
self.attrs['viewBox'] = "0 0 {width} {height}".format(**self.attrs)
g.addTo(self)
self.formatted = True
return self
def writeSvg(self, write):
if not self.formatted:
self.format()
return DiagramItem.writeSvg(self, write)
def parseCSSGrammar(self, text):
token_patterns = {
'keyword': r"[\w-]+\(?",
'type': r"<[\w-]+(\(\))?>",
'char': r"[/,()]",
'literal': r"'(.)'",
'openbracket': r"\[",
'closebracket': r"\]",
'closebracketbang': r"\]!",
'bar': r"\|",
'doublebar': r"\|\|",
'doubleand': r"&&",
'multstar': r"\*",
'multplus': r"\+",
'multhash': r"#",
'multnum1': r"{\s*(\d+)\s*}",
'multnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}",
'multhashnum1': r"#{\s*(\d+)\s*}",
'multhashnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}"
}
class Sequence(DiagramItem):
def __init__(self, *items):
DiagramItem.__init__(self, 'g')
self.items = [wrapString(item) for item in items]
self.needsSpace = True
self.up = 0
self.down = 0
self.height = 0
self.width = 0
for item in self.items:
self.width += item.width + (20 if item.needsSpace else 0)
self.up = max(self.up, item.up - self.height)
self.height += item.height
self.down = max(self.down - item.height, item.down)
if self.items[0].needsSpace:
self.width -= 10
if self.items[-1].needsSpace:
self.width -= 10
addDebug(self)
def __repr__(self):
items = ', '.join(map(repr, self.items))
return 'Sequence(%s)' % items
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
Path(x, y).h(leftGap).addTo(self)
Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
x += leftGap
for i, item in enumerate(self.items):
if item.needsSpace and i > 0:
Path(x, y).h(10).addTo(self)
x += 10
item.format(x, y, item.width).addTo(self)
x += item.width
y += item.height
if item.needsSpace and i < len(self.items) - 1:
Path(x, y).h(10).addTo(self)
x += 10
return self
class Stack(DiagramItem):
def __init__(self, *items):
DiagramItem.__init__(self, 'g')
self.items = [wrapString(item) for item in items]
self.needsSpace = True
self.width = max(item.width + (20 if item.needsSpace else 0)
for item in self.items)
# pretty sure that space calc is totes wrong
if len(self.items) > 1:
self.width += C.AR * 2
self.up = self.items[0].up
self.down = self.items[-1].down
self.height = 0
last = len(self.items) - 1
for i, item in enumerate(self.items):
self.height += item.height
if i > 0:
self.height += max(C.AR * 2, item.up + C.VS)
if i < last:
self.height += max(C.AR * 2, item.down + C.VS)
addDebug(self)
def __repr__(self):
items = ', '.join(repr(item) for item in self.items)
return 'Stack(%s)' % items
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
Path(x, y).h(leftGap).addTo(self)
x += leftGap
xInitial = x
if len(self.items) > 1:
Path(x, y).h(C.AR).addTo(self)
x += C.AR
innerWidth = self.width - C.AR * 2
else:
innerWidth = self.width
for i, item in enumerate(self.items):
item.format(x, y, innerWidth).addTo(self)
x += innerWidth
y += item.height
if i != len(self.items) - 1:
(Path(x, y)
.arc('ne').down(max(0, item.down + C.VS - C.AR * 2))
.arc('es').left(innerWidth)
.arc('nw').down(max(0, self.items[i + 1].up + C.VS - C.AR * 2))
.arc('ws').addTo(self))
y += max(item.down + C.VS, C.AR * 2) + \
max(self.items[i + 1].up + C.VS, C.AR * 2)
x = xInitial + C.AR
if len(self.items) > 1:
Path(x, y).h(C.AR).addTo(self)
x += C.AR
Path(x, y).h(rightGap).addTo(self)
return self
class OptionalSequence(DiagramItem):
def __new__(cls, *items):
if len(items) <= 1:
return Sequence(*items)
else:
return super(OptionalSequence, cls).__new__(cls)
def __init__(self, *items):
DiagramItem.__init__(self, 'g')
self.items = [wrapString(item) for item in items]
self.needsSpace = False
self.width = 0
self.up = 0
self.height = sum(item.height for item in self.items)
self.down = self.items[0].down
heightSoFar = 0
for i, item in enumerate(self.items):
self.up = max(self.up, max(C.AR * 2, item.up + C.VS) - heightSoFar)
heightSoFar += item.height
if i > 0:
self.down = max(self.height + self.down, heightSoFar
+ max(C.AR * 2, item.down + C.VS)) - self.height
itemWidth = item.width + (20 if item.needsSpace else 0)
if i == 0:
self.width += C.AR + max(itemWidth, C.AR)
else:
self.width += C.AR * 2 + max(itemWidth, C.AR) + C.AR
addDebug(self)
def __repr__(self):
items = ', '.join(repr(item) for item in self.items)
return 'OptionalSequence(%s)' % items
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
Path(x, y).right(leftGap).addTo(self)
Path(x + leftGap + self.width, y
+ self.height).right(rightGap).addTo(self)
x += leftGap
upperLineY = y - self.up
last = len(self.items) - 1
for i, item in enumerate(self.items):
itemSpace = 10 if item.needsSpace else 0
itemWidth = item.width + itemSpace
if i == 0:
# Upper skip
(Path(x, y)
.arc('se')
.up(y - upperLineY - C.AR * 2)
.arc('wn')
.right(itemWidth - C.AR)
.arc('ne')
.down(y + item.height - upperLineY - C.AR * 2)
.arc('ws')
.addTo(self))
# Straight line
(Path(x, y)
.right(itemSpace + C.AR)
.addTo(self))
item.format(x + itemSpace + C.AR, y, item.width).addTo(self)
x += itemWidth + C.AR
y += item.height
elif i < last:
# Upper skip
(Path(x, upperLineY)
.right(C.AR * 2 + max(itemWidth, C.AR) + C.AR)
.arc('ne')
.down(y - upperLineY + item.height - C.AR * 2)
.arc('ws')
.addTo(self))
# Straight line
(Path(x, y)
.right(C.AR * 2)
.addTo(self))
item.format(x + C.AR * 2, y, item.width).addTo(self)
(Path(x + item.width + C.AR * 2, y + item.height)
.right(itemSpace + C.AR)
.addTo(self))
# Lower skip
(Path(x, y)
.arc('ne')
.down(item.height + max(item.down + C.VS, C.AR * 2) - C.AR * 2)
.arc('ws')
.right(itemWidth - C.AR)
.arc('se')
.up(item.down + C.VS - C.AR * 2)
.arc('wn')
.addTo(self))
x += C.AR * 2 + max(itemWidth, C.AR) + C.AR
y += item.height
else:
# Straight line
(Path(x, y)
.right(C.AR * 2)
.addTo(self))
item.format(x + C.AR * 2, y, item.width).addTo(self)
(Path(x + C.AR * 2 + item.width, y + item.height)
.right(itemSpace + C.AR)
.addTo(self))
# Lower skip
(Path(x, y)
.arc('ne')
.down(item.height + max(item.down + C.VS, C.AR * 2) - C.AR * 2)
.arc('ws')
.right(itemWidth - C.AR)
.arc('se')
.up(item.down + C.VS - C.AR * 2)
.arc('wn')
.addTo(self))
return self
class AlternatingSequence(DiagramItem):
def __new__(cls, *items):
if len(items) == 2:
return super(AlternatingSequence, cls).__new__(cls)
else:
raise Exception(
"AlternatingSequence takes exactly two arguments got " + len(items))
def __init__(self, *items):
DiagramItem.__init__(self, 'g')
self.items = [wrapString(item) for item in items]
self.needsSpace = False
arc = C.AR
vert = C.VS
first = self.items[0]
second = self.items[1]
arcX = 1 / math.sqrt(2) * arc * 2
arcY = (1 - 1 / math.sqrt(2)) * arc * 2
crossY = max(arc, vert)
crossX = (crossY - arcY) + arcX
firstOut = max(arc + arc, crossY / 2 + arc + arc,
crossY / 2 + vert + first.down)
self.up = firstOut + first.height + first.up
secondIn = max(arc + arc, crossY / 2 + arc + arc,
crossY / 2 + vert + second.up)
self.down = secondIn + second.height + second.down
self.height = 0
firstWidth = (20 if first.needsSpace else 0) + first.width
secondWidth = (20 if second.needsSpace else 0) + second.width
self.width = 2 * arc + max(firstWidth, crossX, secondWidth) + 2 * arc
addDebug(self)
def __repr__(self):
items = ', '.join(repr(item) for item in self.items)
return 'AlternatingSequence(%s)' % items
def format(self, x, y, width):
arc = C.AR
gaps = determineGaps(width, self.width)
Path(x, y).right(gaps[0]).addTo(self)
x += gaps[0]
Path(x + self.width, y).right(gaps[1]).addTo(self)
# bounding box
# Path(x+gaps[0], y).up(self.up).right(self.width).down(self.up+self.down).left(self.width).up(self.down).addTo(self)
first = self.items[0]
second = self.items[1]
# top
firstIn = self.up - first.up
firstOut = self.up - first.up - first.height
Path(x, y).arc('se').up(firstIn - 2 * arc).arc('wn').addTo(self)
first.format(
x
+ 2
* arc,
y
- firstIn,
self.width
- 4
* arc).addTo(self)
Path(x + self.width - 2 * arc, y
- firstOut).arc('ne').down(firstOut - 2 * arc).arc('ws').addTo(self)
# bottom
secondIn = self.down - second.down - second.height
secondOut = self.down - second.down
Path(x, y).arc('ne').down(secondIn - 2 * arc).arc('ws').addTo(self)
second.format(
x
+ 2
* arc,
y
+ secondIn,
self.width
- 4
* arc).addTo(self)
Path(x + self.width - 2 * arc, y
+ secondOut).arc('se').up(secondOut - 2 * arc).arc('wn').addTo(self)
# crossover
arcX = 1 / Math.sqrt(2) * arc * 2
arcY = (1 - 1 / Math.sqrt(2)) * arc * 2
crossY = max(arc, C.VS)
crossX = (crossY - arcY) + arcX
crossBar = (self.width - 4 * arc - crossX) / 2
(Path(x + arc, y - crossY / 2 - arc).arc('ws').right(crossBar)
.arc_8('n', 'cw').ll(crossX - arcX, crossY - arcY).arc_8('sw', 'ccw')
.right(crossBar).arc('ne').addTo(self))
(Path(x + arc, y + crossY / 2 + arc).arc('wn').right(crossBar)
.arc_8('s', 'ccw').ll(crossX - arcX, -(crossY - arcY)).arc_8('nw', 'cw')
.right(crossBar).arc('se').addTo(self))
return self
class Choice(DiagramItem):
def __init__(self, default, *items):
DiagramItem.__init__(self, 'g')
assert default < len(items)
self.default = default
self.items = [wrapString(item) for item in items]
self.width = C.AR * 4 + max(item.width for item in self.items)
self.up = self.items[0].up
self.down = self.items[-1].down
self.height = self.items[default].height
for i, item in enumerate(self.items):
if i in [default - 1, default + 1]:
arcs = C.AR * 2
else:
arcs = C.AR
if i < default:
self.up += max(arcs, item.height + item.down
+ C.VS + self.items[i + 1].up)
elif i == default:
continue
else:
self.down += max(arcs, item.up + C.VS
+ self.items[i - 1].down + self.items[i - 1].height)
# already counted in self.height
self.down -= self.items[default].height
addDebug(self)
def __repr__(self):
items = ', '.join(repr(item) for item in self.items)
return 'Choice(%r, %s)' % (self.default, items)
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
# Hook up the two sides if self is narrower than its stated width.
Path(x, y).h(leftGap).addTo(self)
Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
x += leftGap
innerWidth = self.width - C.AR * 4
default = self.items[self.default]
# Do the elements that curve above
above = self.items[:self.default][::-1]
if above:
distanceFromY = max(
C.AR * 2,
default.up
+ C.VS
+ above[0].down
+ above[0].height)
for i, ni, item in doubleenumerate(above):
Path(x, y).arc('se').up(distanceFromY
- C.AR * 2).arc('wn').addTo(self)
item.format(x + C.AR * 2, y - distanceFromY,
innerWidth).addTo(self)
Path(x + C.AR * 2 + innerWidth, y - distanceFromY + item.height).arc('ne') \
.down(distanceFromY - item.height + default.height - C.AR * 2).arc('ws').addTo(self)
if ni < -1:
distanceFromY += max(
C.AR,
item.up
+ C.VS
+ above[i + 1].down
+ above[i + 1].height)
# Do the straight-line path.
Path(x, y).right(C.AR * 2).addTo(self)
self.items[self.default].format(
x + C.AR * 2, y, innerWidth).addTo(self)
Path(x + C.AR * 2 + innerWidth, y
+ self.height).right(C.AR * 2).addTo(self)
# Do the elements that curve below
below = self.items[self.default + 1:]
if below:
distanceFromY = max(
C.AR * 2,
default.height
+ default.down
+ C.VS
+ below[0].up)
for i, item in enumerate(below):
Path(x, y).arc('ne').down(
distanceFromY - C.AR * 2).arc('ws').addTo(self)
item.format(x + C.AR * 2, y + distanceFromY,
innerWidth).addTo(self)
Path(x + C.AR * 2 + innerWidth, y + distanceFromY + item.height).arc('se') \
.up(distanceFromY - C.AR * 2 + item.height - default.height).arc('wn').addTo(self)
distanceFromY += max(
C.AR,
item.height
+ item.down
+ C.VS
+ (below[i + 1].up if i + 1 < len(below) else 0))
return self
class MultipleChoice(DiagramItem):
def __init__(self, default, type, *items):
DiagramItem.__init__(self, 'g')
assert 0 <= default < len(items)
assert type in ["any", "all"]
self.default = default
self.type = type
self.needsSpace = True
self.items = [wrapString(item) for item in items]
self.innerWidth = max(item.width for item in self.items)
self.width = 30 + C.AR + self.innerWidth + C.AR + 20
self.up = self.items[0].up
self.down = self.items[-1].down
self.height = self.items[default].height
for i, item in enumerate(self.items):
if i in [default - 1, default + 1]:
minimum = 10 + C.AR
else:
minimum = C.AR
if i < default:
self.up += max(minimum, item.height
+ item.down + C.VS + self.items[i + 1].up)
elif i == default:
continue
else:
self.down += max(minimum, item.up + C.VS
+ self.items[i - 1].down + self.items[i - 1].height)
# already counted in self.height
self.down -= self.items[default].height
addDebug(self)
def __repr__(self):
items = ', '.join(map(repr, self.items))
return 'MultipleChoice(%r, %r, %s)' % (self.default, self.type, items)
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
# Hook up the two sides if self is narrower than its stated width.
Path(x, y).h(leftGap).addTo(self)
Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
x += leftGap
default = self.items[self.default]
# Do the elements that curve above
above = self.items[:self.default][::-1]
if above:
distanceFromY = max(
10 + C.AR,
default.up
+ C.VS
+ above[0].down
+ above[0].height)
for i, ni, item in doubleenumerate(above):
(Path(x + 30, y)
.up(distanceFromY - C.AR)
.arc('wn')
.addTo(self))
item.format(x + 30 + C.AR, y - distanceFromY,
self.innerWidth).addTo(self)
(Path(x + 30 + C.AR + self.innerWidth, y - distanceFromY + item.height)
.arc('ne')
.down(distanceFromY - item.height + default.height - C.AR - 10)
.addTo(self))
if ni < -1:
distanceFromY += max(
C.AR,
item.up
+ C.VS
+ above[i + 1].down
+ above[i + 1].height)
# Do the straight-line path.
Path(x + 30, y).right(C.AR).addTo(self)
self.items[self.default].format(
x + 30 + C.AR, y, self.innerWidth).addTo(self)
Path(x + 30 + C.AR + self.innerWidth, y
+ self.height).right(C.AR).addTo(self)
# Do the elements that curve below
below = self.items[self.default + 1:]
if below:
distanceFromY = max(
10 + C.AR,
default.height
+ default.down
+ C.VS
+ below[0].up)
for i, item in enumerate(below):
(Path(x + 30, y)
.down(distanceFromY - C.AR)
.arc('ws')
.addTo(self))
item.format(x + 30 + C.AR, y + distanceFromY,
self.innerWidth).addTo(self)
(Path(x + 30 + C.AR + self.innerWidth, y + distanceFromY + item.height)
.arc('se')
.up(distanceFromY - C.AR + item.height - default.height - 10)
.addTo(self))
distanceFromY += max(
C.AR,
item.height
+ item.down
+ C.VS
+ (below[i + 1].up if i + 1 < len(below) else 0))
text = DiagramItem('g', attrs={"class": "diagram-text"}).addTo(self)
DiagramItem('title', text="take one or more branches, once each, in any order" if self.type
== "any" else "take all branches, once each, in any order").addTo(text)
DiagramItem('path', attrs={
"d": "M {x} {y} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z".format(x=x + 30, y=y - 10),
"class": "diagram-text"
}).addTo(text)
DiagramItem('text', text="1+" if self.type == "any" else "all", attrs={
"x": x + 15,
"y": y + 4,
"class": "diagram-text"
}).addTo(text)
DiagramItem('path', attrs={
"d": "M {x} {y} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z".format(x=x + self.width - 20, y=y - 10),
"class": "diagram-text"
}).addTo(text)
DiagramItem('text', text=u"↺", attrs={
"x": x + self.width - 10,
"y": y + 4,
"class": "diagram-arrow"
}).addTo(text)
return self
class HorizontalChoice(DiagramItem):
def __new__(cls, *items):
if len(items) <= 1:
return Sequence(*items)
else:
return super(HorizontalChoice, cls).__new__(cls)
def __init__(self, *items):
DiagramItem.__init__(self, 'g')
self.items = [wrapString(item) for item in items]
allButLast = self.items[:-1]
middles = self.items[1:-1]
first = self.items[0]
last = self.items[-1]
self.needsSpace = False
self.width = (C.AR # starting track
+ C.AR * 2 * (len(self.items) - 1) # inbetween tracks
+ sum(x.width + (20 if x.needsSpace else 0)
for x in self.items) # items
# needs space to curve up
+ (C.AR if last.height > 0 else 0)
+ C.AR) # ending track
# Always exits at entrance height
self.height = 0
# All but the last have a track running above them
self._upperTrack = max(
C.AR * 2,
C.VS,
max(x.up for x in allButLast) + C.VS
)
self.up = max(self._upperTrack, last.up)
# All but the first have a track running below them
# Last either straight-lines or curves up, so has different calculation
self._lowerTrack = max(
C.VS,
max(x.height + max(x.down + C.VS, C.AR * 2)
for x in middles) if middles else 0,
last.height + last.down + C.VS
)
if first.height < self._lowerTrack:
# Make sure there's at least 2*C.AR room between first exit and
# lower track
self._lowerTrack = max(self._lowerTrack, first.height + C.AR * 2)
self.down = max(self._lowerTrack, first.height + first.down)
addDebug(self)
def format(self, x, y, width):
# Hook up the two sides if self is narrower than its stated width.
leftGap, rightGap = determineGaps(width, self.width)
Path(x, y).h(leftGap).addTo(self)
Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
x += leftGap
first = self.items[0]
last = self.items[-1]
# upper track
upperSpan = (sum(x.width + (20 if x.needsSpace else 0) for x in self.items[:-1])
+ (len(self.items) - 2) * C.AR * 2
- C.AR)
(Path(x, y)
.arc('se')
.up(self._upperTrack - C.AR * 2)
.arc('wn')
.h(upperSpan)
.addTo(self))
# lower track
lowerSpan = (sum(x.width + (20 if x.needsSpace else 0) for x in self.items[1:])
+ (len(self.items) - 2) * C.AR * 2
+ (C.AR if last.height > 0 else 0)
- C.AR)
lowerStart = x + C.AR + first.width + \
(20 if first.needsSpace else 0) + C.AR * 2
(Path(lowerStart, y + self._lowerTrack)
.h(lowerSpan)
.arc('se')
.up(self._lowerTrack - C.AR * 2)
.arc('wn')
.addTo(self))
# Items
for [i, item] in enumerate(self.items):
# input track
if i == 0:
(Path(x, y)
.h(C.AR)
.addTo(self))
x += C.AR
else:
(Path(x, y - self._upperTrack)
.arc('ne')
.v(self._upperTrack - C.AR * 2)
.arc('ws')
.addTo(self))
x += C.AR * 2
# item
itemWidth = item.width + (20 if item.needsSpace else 0)
item.format(x, y, itemWidth).addTo(self)
x += itemWidth
# output track
if i == len(self.items) - 1:
if item.height == 0:
(Path(x, y)
.h(C.AR)
.addTo(self))
else:
(Path(x, y + item.height)
.arc('se')
.addTo(self))
elif i == 0 and item.height > self._lowerTrack:
# Needs to arc up to meet the lower track, not down.
if item.height - self._lowerTrack >= C.AR * 2:
(Path(x, y + item.height)
.arc('se')
.v(self._lowerTrack - item.height + C.AR * 2)
.arc('wn')
.addTo(self))
else:
# Not enough space to fit two arcs
# so just bail and draw a straight line for now.
(Path(x, y + item.height)
.ll(C.AR * 2, self._lowerTrack - item.height)
.addTo(self))
else:
(Path(x, y + item.height)
.arc('ne')
.v(self._lowerTrack - item.height - C.AR * 2)
.arc('ws')
.addTo(self))
return self
def Optional(item, skip=False):
return Choice(0 if skip else 1, Skip(), item)
class OneOrMore(DiagramItem):
def __init__(self, item, repeat=None):
DiagramItem.__init__(self, 'g')
repeat = repeat or Skip()
self.item = wrapString(item)
self.rep = wrapString(repeat)
self.width = max(self.item.width, self.rep.width) + C.AR * 2
self.height = self.item.height
self.up = self.item.up
self.down = max(
C.AR * 2,
self.item.down + C.VS + self.rep.up + self.rep.height + self.rep.down)
self.needsSpace = True
addDebug(self)
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
# Hook up the two sides if self is narrower than its stated width.
Path(x, y).h(leftGap).addTo(self)
Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
x += leftGap
# Draw item
Path(x, y).right(C.AR).addTo(self)
self.item.format(x + C.AR, y, self.width - C.AR * 2).addTo(self)
Path(x + self.width - C.AR, y + self.height).right(C.AR).addTo(self)
# Draw repeat arc
distanceFromY = max(C.AR * 2, self.item.height
+ self.item.down + C.VS + self.rep.up)
Path(x + C.AR, y).arc('nw').down(distanceFromY - C.AR * 2) \
.arc('ws').addTo(self)
self.rep.format(x + C.AR, y + distanceFromY,
self.width - C.AR * 2).addTo(self)
Path(x + self.width - C.AR, y + distanceFromY + self.rep.height).arc('se') \
.up(distanceFromY - C.AR * 2 + self.rep.height - self.item.height).arc('en').addTo(self)
return self
def __repr__(self):
return 'OneOrMore(%r, repeat=%r)' % (self.item, self.rep)
def ZeroOrMore(item, repeat=None, skip=False):
result = Optional(OneOrMore(item, repeat), skip)
return result
class Start(DiagramItem):
def __init__(self, type="simple", label=None):
DiagramItem.__init__(self, 'g')
if label:
self.width = max(20, len(label) * C.CHAR_WIDTH + 10)
else:
self.width = 20
self.up = 10
self.down = 10
self.type = type
self.label = label
addDebug(self)
def format(self, x, y, _width):
path = Path(x, y - 10)
if self.type == "complex":
path.down(20).m(0, -10).right(self.width).addTo(self)
else:
path.down(20).m(10, -20).down(20).m(-10,
- 10).right(self.width).addTo(self)
if self.label:
DiagramItem('text', attrs={
"x": x, "y": y - 15, "style": "text-anchor:start"}, text=self.label).addTo(self)
return self
def __repr__(self):
return 'Start(type=%r, label=%r)' % (self.type, self.label)
class End(DiagramItem):
def __init__(self, type="simple"):
DiagramItem.__init__(self, 'path')
self.width = 20
self.up = 10
self.down = 10
self.type = type
addDebug(self)
def format(self, x, y, _width):
if self.type == "simple":
self.attrs['d'] = 'M {0} {1} h 20 m -10 -10 v 20 m 10 -20 v 20'.format(
x, y)
elif self.type == "complex":
self.attrs['d'] = 'M {0} {1} h 20 m 0 -10 v 20'
return self
def __repr__(self):
return 'End(type=%r)' % self.type
class Terminal(DiagramItem):
def __init__(self, text, href=None, title=None):
DiagramItem.__init__(self, 'g', {'class': 'terminal'})
self.text = text
self.href = href
self.title = title
self.width = len(text) * C.CHAR_WIDTH + 20
self.up = 11
self.down = 11
self.needsSpace = True
addDebug(self)
def __repr__(self):
return 'Terminal(%r, href=%r, title=%r)' % (
self.text, self.href, self.title)
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
# Hook up the two sides if self is narrower than its stated width.
Path(x, y).h(leftGap).addTo(self)
Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width,
'height': self.up + self.down, 'rx': 10, 'ry': 10}).addTo(self)
text = DiagramItem('text', {'x': x + width / 2, 'y': y + 4}, self.text)
if self.href is not None:
a = DiagramItem('a', {'xlink:href': self.href}, text).addTo(self)
text.addTo(a)
else:
text.addTo(self)
if self.title is not None:
DiagramItem('title', {}, self.title).addTo(self)
return self
class NonTerminal(DiagramItem):
def __init__(self, text, href=None, title=None):
DiagramItem.__init__(self, 'g', {'class': 'non-terminal'})
self.text = text
self.href = href
self.title = title
self.width = len(text) * C.CHAR_WIDTH + 20
self.up = 11
self.down = 11
self.needsSpace = True
addDebug(self)
def __repr__(self):
return 'NonTerminal(%r, href=%r, title=%r)' % (
self.text, self.href, self.title)
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
# Hook up the two sides if self is narrower than its stated width.
Path(x, y).h(leftGap).addTo(self)
Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width,
'height': self.up + self.down}).addTo(self)
text = DiagramItem('text', {'x': x + width / 2, 'y': y + 4}, self.text)
if self.href is not None:
a = DiagramItem('a', {'xlink:href': self.href}, text).addTo(self)
text.addTo(a)
else:
text.addTo(self)
if self.title is not None:
DiagramItem('title', {}, self.title).addTo(self)
return self
class Comment(DiagramItem):
def __init__(self, text, href=None, title=None):
DiagramItem.__init__(self, 'g')
self.text = text
self.href = href
self.title = title
self.width = len(text) * C.COMMENT_CHAR_WIDTH + 10
self.up = 11
self.down = 11
self.needsSpace = True
addDebug(self)
def __repr__(self):
return 'Comment(%r, href=%r, title=%r)' % (
self.text, self.href, self.title)
def format(self, x, y, width):
leftGap, rightGap = determineGaps(width, self.width)
# Hook up the two sides if self is narrower than its stated width.
Path(x, y).h(leftGap).addTo(self)
Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
text = DiagramItem(
'text', {'x': x + width / 2, 'y': y + 5, 'class': 'comment'}, self.text)
if self.href is not None:
a = DiagramItem('a', {'xlink:href': self.href}, text).addTo(self)
text.addTo(a)
else:
text.addTo(self)
if self.title is not None:
DiagramItem('title', {}, self.title).addTo(self)
return self
class Skip(DiagramItem):
def __init__(self):
DiagramItem.__init__(self, 'g')
self.width = 0
self.up = 0
self.down = 0
addDebug(self)
def format(self, x, y, width):
Path(x, y).right(width).addTo(self)
return self
def __repr__(self):
return 'Skip()'
def show_diagram(graph, log=False):
with io.StringIO() as f:
d = Diagram(graph)
if log:
print(d)
d.writeSvg(f.write)
mysvg = f.getvalue()
return mysvg
```</details>
 本项目的内容受 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-sa/4.0/) 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受 [MIT 许可协议](https://github.com/uds-se/fuzzingbook/blob/master/LICENSE.md#mit-license) 的许可。 [最后更改:2023-11-11 18:18:06+01:00](https://github.com/uds-se/fuzzingbook/commits/master/notebooks/RailroadDiagrams.ipynb) • 引用 • [版权信息](https://cispa.de/en/impressum)
## 如何引用此作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "[Railroad Diagrams](https://www.fuzzingbook.org/html/RailroadDiagrams.html)". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "[The Fuzzing Book](https://www.fuzzingbook.org/)", [`www.fuzzingbook.org/html/RailroadDiagrams.html`](https://www.fuzzingbook.org/html/RailroadDiagrams.html). Retrieved 2023-11-11 18:18:06+01:00.
```py
@incollection{fuzzingbook2023:RailroadDiagrams,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Railroad Diagrams},
year = {2023},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/RailroadDiagrams.html}},
note = {Retrieved 2023-11-11 18:18:06+01:00},
url = {https://www.fuzzingbook.org/html/RailroadDiagrams.html},
urldate = {2023-11-11 18:18:06+01:00}
}


浙公网安备 33010602011771号