Add shared bindgen core

Language-agnostic model, naming, and loader consumed by the
frida-python and frida-node binding generators.
This commit is contained in:
Ole André Vadla Ravnås authored and Ole André Vadla Ravnås committed 2026-07-15 23:02:37 +02:00
commit df17879d92
7 files changed
+990

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Ole André Vadla Ravnås and Frida contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
# frida-bindgen
Language-agnostic core for Frida's `.gir`-driven binding generators.
`frida_bindgen_core` holds the shared substrate consumed by frida-python and
frida-node: the introspection model parsed from Frida's GObject-Introspection
`.gir` files, the naming helpers, and the loader. Each binding vendors this
package as a git submodule and layers its own language-specific subclasses,
code generator, and customizations on top via the `Factory` seam.
+8
View File
@@ -0,0 +1,8 @@
from . import model, naming
from .loader import compute_model
from .model import (Constructor, Direction, Enumeration, EnumerationMember,
Factory, InterfaceObjectType, Method, Model, Namespace,
ObjectType, Parameter, Procedure, Property, ReturnValue,
Signal, TransferOwnership, Type, ClassObjectType,
parse_gir)
from .naming import to_camel_case, to_macro_case, to_pascal_case, to_snake_case
+57
View File
@@ -0,0 +1,57 @@
from __future__ import annotations
from collections import OrderedDict
from pathlib import Path
from typing import List
from .model import Factory, Model, parse_gir
def compute_model(
frida_gir: Path,
glib_gir: Path,
gobject_gir: Path,
gio_gir: Path,
customizations: object,
factory: Factory,
included_gio_object_types: List[str],
included_gio_enumerations: List[str],
seed_object_first: bool,
) -> Model:
glib = parse_gir(glib_gir, [], factory)
gobject = parse_gir(gobject_gir, [glib], factory)
gio = parse_gir(gio_gir, [glib, gobject], factory)
frida = parse_gir(frida_gir, [glib, gobject, gio], factory)
object_types = OrderedDict()
if seed_object_first:
object_types["Object"] = gobject.object_types["Object"]
object_types.update(frida.object_types)
else:
object_types.update(frida.object_types)
object_types["Object"] = gobject.object_types["Object"]
for t in included_gio_object_types:
object_types[t] = gio.object_types[t]
enumerations = OrderedDict(frida.enumerations)
for t in included_gio_enumerations:
enumerations[t] = gio.enumerations[t]
model = factory.model(
frida.namespace,
object_types,
enumerations,
customizations,
error_domain=frida.error_domain,
factory=factory,
)
for t in object_types.values():
t.model = model
for t in enumerations.values():
t.model = model
if model.error_domain is not None:
model.error_domain.model = model
return model
+853
View File
@@ -0,0 +1,853 @@
from __future__ import annotations
import xml.etree.ElementTree as ET
from collections import OrderedDict
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
from typing import (Callable, Iterator, List, Optional, Sequence, Tuple)
from .naming import to_snake_case
CORE_NAMESPACE = "http://www.gtk.org/introspection/core/1.0"
C_NAMESPACE = "http://www.gtk.org/introspection/c/1.0"
GLIB_NAMESPACE = "http://www.gtk.org/introspection/glib/1.0"
GIR_NAMESPACES = {"": CORE_NAMESPACE, "glib": GLIB_NAMESPACE}
CORE_TAG_PREFIX = f"{{{CORE_NAMESPACE}}}"
NUMERIC_GIR_TYPES = {
"gsize",
"gssize",
"gint",
"guint",
"glong",
"gulong",
"gint8",
"gint16",
"gint32",
"gint64",
"guint8",
"guint16",
"guint32",
"guint64",
"gfloat",
"gdouble",
"GType",
"GQuark",
}
PRIMITIVE_GIR_TYPES = NUMERIC_GIR_TYPES | {
"gpointer",
"gboolean",
"gchar",
"utf8",
"utf8[]",
}
ResolveTypeCallback = Callable[[str], Tuple[str, ET.Element]]
@dataclass
class Factory:
class_object_type: Callable
interface_object_type: Callable
constructor: Callable
method: Callable
parameter: Callable
return_value: Callable
signal: Callable
property_: Callable
enumeration: Callable
enumeration_member: Callable
model: Callable
@dataclass
class Model:
namespace: Namespace
_object_types: OrderedDict
enumerations: OrderedDict
customizations: Optional[object] = None
error_domain: Optional[object] = None
factory: Optional[Factory] = None
@cached_property
def object_types(self) -> OrderedDict:
result = OrderedDict()
if self.customizations is not None:
type_customizations = self.customizations.type_customizations
else:
type_customizations = {}
for k, v in self._object_types.items():
custom = type_customizations.get(k)
if custom is None or not custom.drop:
result[k] = v
return result
def resolve_object_type(self, name: str) -> ObjectType:
bare_name = name.split(".", maxsplit=1)[-1]
return self.object_types[bare_name]
@dataclass
class Namespace:
name: str
identifier_prefixes: str
element: ET.Element
@cached_property
def type_elements(self):
result = {}
for toplevel in self.element.findall("./*[@name]", GIR_NAMESPACES):
name = toplevel.get("name")
result[name] = toplevel
for callback in toplevel.findall("./callback", GIR_NAMESPACES):
result[name + callback.get("name")] = callback
return result
@dataclass
class ObjectType:
name: str
c_type: str
get_type: str
type_struct: str
_parent: Optional[str]
_constructors: List[ET.Element]
_methods: List[ET.Element]
_properties: List[ET.Element]
_signals: List[ET.Element]
resolve_type: ResolveTypeCallback
model: Optional[Model]
@cached_property
def parent(self) -> ObjectType:
if self._parent is None:
return None
return self.model.resolve_object_type(self._parent)
@cached_property
def is_frida_options(self) -> bool:
return self.c_type.startswith("Frida") and self.c_type.endswith("Options")
@cached_property
def is_frida_list(self) -> bool:
return self.c_type.startswith("Frida") and self.c_type.endswith("List")
@cached_property
def customizations(self):
return self.model.customizations.type_customizations.get(self.name)
@cached_property
def constructors(self) -> List[Constructor]:
factory = self.model.factory
constructors = []
custom = self.customizations
for element in self._constructors:
if element.get("introspectable") == "0" or element.get("deprecated") == "1":
continue
name = element.get("name")
if custom is not None:
ccust = custom.constructor
if ccust is not None and ccust.drop:
continue
(
c_identifier,
finish_c_identifier,
param_list,
has_closure_param,
throws,
result_element,
) = extract_callable_details(element, element, self, self.resolve_type)
if has_closure_param or finish_c_identifier is not None:
continue
constructors.append(
factory.constructor(
name, c_identifier, finish_c_identifier, param_list, throws, self
)
)
return constructors
@cached_property
def methods(self) -> List[Method]:
factory = self.model.factory
methods = []
c_prop_names = {prop.c_name for prop in self.properties}
custom = self.customizations
for element in self._methods:
name = element.get("name")
if (
element.get("introspectable") == "0"
or name.startswith("_")
or name.endswith("_sync")
or name.endswith("_finish")
):
continue
if custom is not None:
mcust = custom.methods.get(name, None)
if mcust is not None and mcust.drop:
continue
finish_func = element.get(f"{{{GLIB_NAMESPACE}}}finish-func")
if finish_func is None:
finish_func = f"{name}_finish"
result_element = next(
(m for m in self._methods if m.get("name") == finish_func), element
)
(
c_identifier,
finish_c_identifier,
param_list,
has_closure_param,
throws,
result_element,
) = extract_callable_details(
element, result_element, self, self.resolve_type
)
if has_closure_param:
continue
retval_element = result_element.find(".//return-value", GIR_NAMESPACES)
rettype = extract_type_from_entity(retval_element, self.resolve_type)
if rettype is not None:
if rettype.is_frida_options:
continue
nullable = retval_element.get("nullable") == "1"
ownership_val = retval_element.get("transfer-ownership")
transfer_ownership = (
TransferOwnership[ownership_val]
if ownership_val is not None
else TransferOwnership.none
)
retval = factory.return_value(
rettype, nullable, transfer_ownership, self
)
else:
retval = None
if element.get(f"{{{GLIB_NAMESPACE}}}get-property") is not None:
is_property_accessor = True
else:
tokens = name.split("_", maxsplit=1)
is_property_accessor = (
len(tokens) == 2
and tokens[0] in {"get", "set"}
and tokens[1] in c_prop_names
)
methods.append(
factory.method(
name,
c_identifier,
finish_c_identifier,
param_list,
throws,
retval,
is_property_accessor,
self,
)
)
return methods
@cached_property
def properties(self) -> List[Property]:
factory = self.model.factory
properties = []
custom = self.customizations
for element in self._properties:
name = element.get("name")
if custom is not None:
pcust = custom.properties.get(name, None)
if pcust is not None and pcust.drop:
continue
c_name = name.replace("-", "_")
type = extract_type_from_entity(element, self.resolve_type)
if type.is_frida_options:
continue
writable = element.get("writable") == "1"
construct_only = element.get("construct-only") == "1"
getter = element.get("getter")
if getter is None:
getter = f"get_{c_name}"
setter = element.get("setter")
if setter is None and writable and not construct_only:
setter = f"set_{c_name}"
properties.append(
factory.property_(
name,
c_name,
type,
writable,
construct_only,
getter,
setter,
self,
)
)
return properties
@cached_property
def signals(self) -> List[Signal]:
factory = self.model.factory
signals = []
custom = self.customizations
for element in self._signals:
name = element.get("name")
if custom is not None:
scust = custom.signals.get(name, None)
if scust is not None and scust.drop:
continue
c_name = name.replace("-", "_")
param_list = extract_parameters(
element.findall("./parameters/parameter", GIR_NAMESPACES),
nullable_implies_optional=False,
object_type=self,
resolve_type=self.resolve_type,
)
signals.append(factory.signal(name, c_name, param_list, self))
return signals
@dataclass
class ClassObjectType(ObjectType):
pass
@dataclass
class InterfaceObjectType(ObjectType):
pass
@dataclass
class Procedure:
name: str
c_identifier: str
finish_c_identifier: Optional[str]
parameters: List[Parameter]
throws: bool
@property
def is_async(self) -> bool:
return self.finish_c_identifier is not None
@cached_property
def input_parameters(self) -> List[Parameter]:
return [p for p in self.parameters if p.direction != Direction.OUT]
@dataclass
class Constructor(Procedure):
object_type: ObjectType
@dataclass
class Method(Procedure):
return_value: Optional[ReturnValue]
is_property_accessor: bool
object_type: ObjectType
@dataclass
class Property:
name: str
c_name: str
type: Type
writable: bool
construct_only: bool
getter: Optional[str]
setter: Optional[str]
object_type: ObjectType
@dataclass
class Signal:
name: str
c_name: str
parameters: List[Parameter]
object_type: ObjectType
TransferOwnership = Enum("TransferOwnership", ["none", "full", "container"])
@dataclass
class Parameter:
name: str
type: Type
optional: bool
nullable: bool
transfer_ownership: TransferOwnership
direction: Direction
object_type: ObjectType
@dataclass
class ReturnValue:
type: Type
nullable: bool
transfer_ownership: TransferOwnership
object_type: ObjectType
@dataclass
class Type:
name: str
nick: str
c: str
default_value: Optional[str]
copy_func: Optional[str]
destroy_func: Optional[str]
@cached_property
def is_frida_options(self) -> bool:
return self.c.startswith("Frida") and self.c.endswith("Options *")
@cached_property
def from_pointer_func(self) -> Optional[str]:
if self.name in {"gssize", "gsize", "glong", "gulong", "gint64", "guint64"}:
return "GPOINTER_TO_SIZE"
if self.name in {"gint", "gint8", "gint16", "gint32"}:
return "GPOINTER_TO_INT"
if self.name in {"gboolean", "guint", "guint8", "guint16", "guint32"}:
return "GPOINTER_TO_UINT"
return None
@cached_property
def to_pointer_func(self) -> Optional[str]:
if self.name in {"gssize", "gsize", "glong", "gulong", "gint64", "guint64"}:
return "GSIZE_TO_POINTER"
if self.name in {"gint", "gint8", "gint16", "gint32"}:
return "GINT_TO_POINTER"
if self.name in {"gboolean", "guint", "guint8", "guint16", "guint32"}:
return "GUINT_TO_POINTER"
return None
class Direction(Enum):
IN = "in"
OUT = "out"
INOUT = "inout"
@dataclass
class Enumeration:
name: str
c_type: str
get_type: str
_members: List[ET.Element]
model: Optional[Model]
@cached_property
def members(self) -> List[EnumerationMember]:
factory = self.model.factory
members = []
for element in self._members:
c_identifier = element.get(f"{{{C_NAMESPACE}}}identifier")
members.append(
factory.enumeration_member(element.get("name"), c_identifier, self)
)
return members
@cached_property
def customizations(self):
return self.model.customizations.type_customizations.get(self.name)
@dataclass
class EnumerationMember:
name: str
c_identifier: Optional[str]
enumeration: Enumeration
def parse_gir(
file_path: str, dependencies: Sequence[Model], factory: Factory
) -> Model:
tree = ET.parse(file_path)
el = tree.getroot().find("./namespace", GIR_NAMESPACES)
namespace = Namespace(
el.get("name"), el.get(f"{{{C_NAMESPACE}}}identifier-prefixes"), el
)
def resolve_type(name: str) -> Tuple[str, ET.Element]:
assert (
name not in PRIMITIVE_GIR_TYPES
), f"unexpectedly asked to resolve primitive type: {name}"
tokens = name.split(".", maxsplit=1)
if len(tokens) == 2:
ns_name, bare_name = tokens
if ns_name == namespace.name:
ns = namespace
else:
ns = next(
(
dep.namespace
for dep in dependencies
if dep.namespace.name == ns_name
),
None,
)
if ns is None:
assert ns is not None, f"unable to resolve namespace {ns_name}"
else:
ns = namespace
bare_name = name
qualified_name = f"{ns.name}.{bare_name}"
element = ns.type_elements.get(bare_name)
assert element is not None, f"unable to resolve type {bare_name}"
return (qualified_name, element)
object_types = OrderedDict()
for element in namespace.element.findall("./class", GIR_NAMESPACES):
name = element.get("name")
c_type = element.get(f"{{{C_NAMESPACE}}}type")
get_type = element.get(f"{{{GLIB_NAMESPACE}}}get-type")
type_struct = element.get(f"{{{GLIB_NAMESPACE}}}type-struct")
if type_struct is not None:
type_struct = namespace.identifier_prefixes + type_struct
else:
type_struct = c_type + "Class"
parent = element.get("parent")
if parent is not None:
parent, _ = resolve_type(parent)
constructors = element.findall(".//constructor", GIR_NAMESPACES)
methods = element.findall(".//method", GIR_NAMESPACES)
properties = element.findall(".//property", GIR_NAMESPACES)
signals = element.findall(".//glib:signal", GIR_NAMESPACES)
implements = [
e.get("name") for e in element.findall(".//implements", GIR_NAMESPACES)
]
object_types[name] = factory.class_object_type(
name=name,
c_type=c_type,
get_type=get_type,
type_struct=type_struct,
parent=parent,
constructors=constructors,
methods=methods,
properties=properties,
signals=signals,
implements=implements,
resolve_type=resolve_type,
model=None,
)
for element in namespace.element.findall("./interface", GIR_NAMESPACES):
name = element.get("name")
c_type = element.get(f"{{{C_NAMESPACE}}}type")
get_type = element.get(f"{{{GLIB_NAMESPACE}}}get-type")
type_struct = element.get(f"{{{GLIB_NAMESPACE}}}type-struct")
if type_struct is not None:
type_struct = namespace.identifier_prefixes + type_struct
else:
type_struct = c_type + "Iface"
prereq = element.find(".//prerequisite", GIR_NAMESPACES)
parent = prereq.get("name") if prereq is not None else None
if parent is not None:
parent, _ = resolve_type(parent)
constructors = []
methods = element.findall(".//method", GIR_NAMESPACES)
properties = element.findall(".//property", GIR_NAMESPACES)
signals = element.findall(".//glib:signal", GIR_NAMESPACES)
object_types[name] = factory.interface_object_type(
name=name,
c_type=c_type,
get_type=get_type,
type_struct=type_struct,
parent=parent,
constructors=constructors,
methods=methods,
properties=properties,
signals=signals,
resolve_type=resolve_type,
model=None,
)
enumerations = OrderedDict()
error_domain = None
for element in namespace.element.findall("./enumeration", GIR_NAMESPACES):
enum_name = element.get("name")
enum_c_type = element.get(f"{{{C_NAMESPACE}}}type")
get_type = element.get(f"{{{GLIB_NAMESPACE}}}get-type")
members = element.findall(".//member", GIR_NAMESPACES)
enumeration = factory.enumeration(
enum_name, enum_c_type, get_type, members, None
)
if element.get(f"{{{GLIB_NAMESPACE}}}error-domain") is not None:
error_domain = enumeration
continue
enumerations[enum_name] = enumeration
model = factory.model(
namespace,
object_types,
enumerations,
error_domain=error_domain,
factory=factory,
)
for t in object_types.values():
t.model = model
for t in enumerations.values():
t.model = model
return model
def extract_callable_details(
element: ET.Element,
result_element: ET.Element,
object_type: ObjectType,
resolve_type: ResolveTypeCallback,
) -> Tuple[str, Optional[str], List[Parameter], bool, bool, ET.Element]:
c_identifier = element.get(f"{{{C_NAMESPACE}}}identifier")
parameters = element.findall("./parameters/parameter", GIR_NAMESPACES)
full_param_list = extract_parameters(
parameters,
nullable_implies_optional=True,
object_type=object_type,
resolve_type=resolve_type,
)
param_list = list(all_regular_parameters(full_param_list))
has_closure_param = any((param.get("closure") == "1" for param in parameters))
is_async = any(
param.type.name == "Gio.AsyncReadyCallback" for param in full_param_list
)
if not is_async:
result_element = element
finish_c_identifier = (
result_element.get(f"{{{C_NAMESPACE}}}identifier") if is_async else None
)
throws = result_element.get("throws") == "1"
return (
c_identifier,
finish_c_identifier,
param_list,
has_closure_param,
throws,
result_element,
)
def extract_parameters(
parameter_elements: List[ET.Element],
nullable_implies_optional: bool,
object_type: ObjectType,
resolve_type: ResolveTypeCallback,
) -> List[Parameter]:
factory = object_type.model.factory
entries = []
for param in parameter_elements:
nullable = param.get("nullable") == "1"
entries.append((param, nullable))
last_required_index = None
for i, (param, nullable) in enumerate(entries):
optional = nullable and nullable_implies_optional
if not optional:
last_required_index = i
param_list = []
for i, (param, nullable) in enumerate(entries):
name = param.get("name")
type = extract_type_from_entity(param, resolve_type)
if last_required_index is None or i > last_required_index:
optional = nullable and nullable_implies_optional
else:
optional = False
ownership_val = param.get("transfer-ownership")
transfer_ownership = (
TransferOwnership[ownership_val]
if ownership_val is not None
else TransferOwnership.none
)
raw_direction = param.get("direction")
direction = (
Direction(raw_direction) if raw_direction is not None else Direction.IN
)
param_list.append(
factory.parameter(
name,
type,
optional,
nullable,
transfer_ownership,
direction,
object_type,
)
)
return param_list
def all_regular_parameters(parameters: List[Parameter]) -> Iterator[Parameter]:
callback_index = None
for i, param in enumerate(parameters):
if param.type.name == "Gio.AsyncReadyCallback":
callback_index = i
continue
if callback_index is not None and i == callback_index + 1:
continue
yield param
def extract_type_from_entity(
parent_element: ET.Element, resolve_type: ResolveTypeCallback
) -> Optional[Type]:
child = parent_element.find("type", GIR_NAMESPACES)
if child is None:
child = parent_element.find("array", GIR_NAMESPACES)
assert child is not None
element_type = extract_type_from_entity(child, resolve_type)
if element_type.name == "utf8":
return Type(
"utf8[]",
"strv",
"gchar **",
"NULL",
"g_strdupv",
"g_strfreev",
)
elif element_type.name == "gchar":
return Type("char[]", "chararray", "gchar *", "NULL", "NULL", "NULL")
elif element_type.name == "GObject.Value":
return Type("Value[]", "valuearray", "GValue *", "NULL", "NULL", "NULL")
else:
assert (
element_type.name == "guint8"
), f"unsupported array type: {element_type.name}"
return Type("uint8[]", "bytearray", "guint8 *", "NULL", "NULL", "NULL")
return parse_type(child, resolve_type)
def parse_type(
element: ET.Element, resolve_type: ResolveTypeCallback
) -> Optional[Type]:
name = element.get("name")
assert name is not None
if name == "none":
return None
is_primitive = name in PRIMITIVE_GIR_TYPES
c_type = element.get(f"{{{C_NAMESPACE}}}type")
core_tag = None
if is_primitive:
type_element = element
if c_type is None:
c_type = name
else:
name, type_element = resolve_type(name)
if type_element.tag.startswith(CORE_TAG_PREFIX):
core_tag = type_element.tag[len(CORE_TAG_PREFIX) :]
c_type = type_element.get(f"{{{C_NAMESPACE}}}type")
if core_tag in {"class", "interface", "record"}:
c_type += "*"
nick = type_nick_from_name(name, element, resolve_type)
c = c_type.replace("*", " *")
default_value = "NULL" if "*" in c else None
if name == "utf8":
copy_func = "g_strdup"
destroy_func = "g_free"
elif name == "utf8[]":
copy_func = "g_strdupv"
destroy_func = "g_strfreev"
elif name == "GLib.HashTable":
copy_func = "g_hash_table_ref"
destroy_func = "g_hash_table_unref"
elif name == "GLib.Quark":
copy_func = None
destroy_func = None
elif name == "GObject.Value":
copy_func = "g_value_copy"
destroy_func = "g_value_reset"
elif name == "GObject.Closure":
copy_func = "g_closure_ref"
destroy_func = "g_closure_unref"
elif core_tag in {"class", "interface"}:
copy_func = "g_object_ref"
destroy_func = "g_object_unref"
elif is_primitive or core_tag in {"bitfield", "callback", "enumeration"}:
copy_func = None
destroy_func = None
else:
copy_func = type_element.get("copy-function")
destroy_func = type_element.get("free-function")
assert (
destroy_func is not None
), f"unable to resolve destroy function for {name}, core_tag={core_tag}"
return Type(name, nick, c, default_value, copy_func, destroy_func)
def type_nick_from_name(
name: str, element: ET.Element, resolve_type: ResolveTypeCallback
) -> str:
if name == "GLib.PollFD":
return "pollfd"
tokens = name.split(".", maxsplit=1)
if len(tokens) == 1:
result = tokens[0]
if result.startswith("g"):
result = result[1:]
else:
result = to_snake_case(tokens[1])
if result == "hash_table":
key_type = parse_type(element[0], resolve_type)
value_type = parse_type(element[1], resolve_type)
assert (
key_type.name == "utf8" and value_type.name == "GLib.Variant"
), "only GHashTable<string, Variant> is supported for now"
result = "vardict"
return result
+40
View File
@@ -0,0 +1,40 @@
def to_snake_case(name: str) -> str:
result = []
i = 0
n = len(name)
while i < n:
if name[i].isupper():
if i > 0:
result.append("_")
start = i
if i + 1 < n and name[i + 1].islower():
while i + 1 < n and name[i + 1].islower():
i += 1
else:
while i + 1 < n and name[i + 1].isupper():
i += 1
if i + 1 < n:
i -= 1
result.append(name[start : i + 1].lower())
else:
result.append(name[i])
i += 1
return "".join(result)
def to_pascal_case(name: str) -> str:
return "".join(word.capitalize() for word in name.split("_"))
def to_camel_case(name: str) -> str:
words = name.split("_")
return words[0] + "".join(word.capitalize() for word in words[1:])
def to_macro_case(identifier: str) -> str:
result = []
for i, char in enumerate(identifier):
if char.isupper() and i != 0:
result.append("_")
result.append(char)
return "".join(result).upper()