File size: 13,288 Bytes
fec6d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""
PHP unserialize β€” parse PHP serialized strings into Python objects.
Usage:
    python php_unserialize.py 'a:2:{s:3:"foo";i:1;s:3:"bar";b:1;}'
    or import and call php_unserialize(data)
"""

import sys
import json


def php_unserialize(data: str):
    """Deserialize a PHP serialized string into a Python object."""
    data = data.strip()
    obj, pos = _parse(data, 0)
    return obj


# ── Internal parser ──────────────────────────────────────────────────────────

def _parse(s: str, pos: int):
    if pos >= len(s):
        raise ValueError(f"Unexpected end of data at position {pos}")

    token = s[pos]

    if token == 'N':                        # null  β†’  N;
        return None, pos + 2               # skip 'N' and ';'

    if token == 'b':                        # bool  β†’  b:0; or b:1;
        val = s[pos + 2] == '1'
        return val, pos + 4

    if token == 'i':                        # int   β†’  i:123;
        end = s.index(';', pos)
        return int(s[pos + 2:end]), end + 1

    if token == 'd':                        # float β†’  d:1.5;
        end = s.index(';', pos)
        return float(s[pos + 2:end]), end + 1

    if token == 's':                        # string β†’ s:3:"foo";
        colon = s.index(':', pos + 2)
        length = int(s[pos + 2:colon])
        # skip colon and opening quote
        start = colon + 2
        value = s[start:start + length]
        return value, start + length + 2    # skip closing '";'

    if token == 'a':                        # array  β†’ a:2:{...}
        colon = s.index(':', pos + 2)
        count = int(s[pos + 2:colon])
        pos = colon + 2                     # skip past '{'
        result = {}
        for _ in range(count):
            key, pos = _parse(s, pos)
            val, pos = _parse(s, pos)
            result[key] = val
        return result, pos + 1             # skip '}'

    if token == 'O':                        # object β†’ O:9:"ClassName":2:{...}
        # class name length
        colon1 = s.index(':', pos + 2)
        cls_len = int(s[pos + 2:colon1])
        cls_start = colon1 + 2
        class_name = s[cls_start:cls_start + cls_len]
        # property count
        colon2 = s.index(':', cls_start + cls_len + 2)
        prop_count = int(s[cls_start + cls_len + 2:colon2])
        pos = colon2 + 2                   # skip past '{'
        props = {}
        for _ in range(prop_count):
            key, pos = _parse(s, pos)
            val, pos = _parse(s, pos)
            props[key] = val
        result = {"__class__": class_name, **props}
        return result, pos + 1            # skip '}'

    if token == 'C':                        # custom serialized β†’ C:classname:len:{data}
        colon1 = s.index(':', pos + 2)
        cls_len = int(s[pos + 2:colon1])
        cls_start = colon1 + 2
        class_name = s[cls_start:cls_start + cls_len]
        colon2 = s.index(':', cls_start + cls_len + 1)
        data_len = int(s[cls_start + cls_len + 1:colon2])
        data_start = colon2 + 2
        raw = s[data_start:data_start + data_len]
        result = {"__class__": class_name, "__data__": raw}
        return result, data_start + data_len + 1

    raise ValueError(f"Unknown type token '{token}' at position {pos}")


# ── Serializer ───────────────────────────────────────────────────────────────

def php_serialize(obj) -> str:
    """Serialize a Python object back into a PHP serialized string."""
    if obj is None:
        return 'N;'
    if isinstance(obj, bool):
        return f'b:{1 if obj else 0};'
    if isinstance(obj, int):
        return f'i:{obj};'
    if isinstance(obj, float):
        return f'd:{obj};'
    if isinstance(obj, str):
        return f's:{len(obj.encode("utf-8"))}:"{obj}";'
    if isinstance(obj, (list, tuple)):
        parts = ''.join(php_serialize(i) + php_serialize(v) for i, v in enumerate(obj))
        return f'a:{len(obj)}:{{{parts}}}'
    if isinstance(obj, dict):
        # Object with __class__ key β†’ O type
        if '__class__' in obj:
            class_name = obj['__class__']
            props = {k: v for k, v in obj.items() if k != '__class__'}
            parts = ''.join(php_serialize(k) + php_serialize(v) for k, v in props.items())
            return f'O:{len(class_name)}:"{class_name}":{len(props)}:{{{parts}}}'
        parts = ''.join(php_serialize(k) + php_serialize(v) for k, v in obj.items())
        return f'a:{len(obj)}:{{{parts}}}'
    raise TypeError(f"Cannot serialize type: {type(obj)}")


# ── Input string ─────────────────────────────────────────────────────────────

PHP_STRING = 'a:4:{s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"R4IF-ClDHXxblLcG0L7MgsLvfBIMAvi_skumNFQwkDc";}s:6:"module";a:276:{s:7:"address";i:0;s:13:"admin_toolbar";i:0;s:19:"admin_toolbar_tools";i:0;s:23:"advanced_text_formatter";i:0;s:10:"autologout";i:0;s:3:"ban";i:0;s:8:"big_pipe";i:0;s:5:"blazy";i:0;s:5:"block";i:0;s:13:"block_content";i:0;s:11:"block_field";i:0;s:17:"bootstrap_library";i:0;s:10:"breakpoint";i:0;s:13:"campaign_erun";i:0;s:16:"campaign_lead_ad";i:0;s:22:"campaign_sportify_2021";i:0;s:7:"captcha";i:0;s:9:"ckeditor5";i:0;s:17:"classy_paragraphs";i:0;s:8:"colorbox";i:0;s:7:"comment";i:0;s:6:"config";i:0;s:13:"config_filter";i:0;s:13:"config_ignore";i:0;s:12:"config_pages";i:0;s:12:"config_split";i:0;s:13:"config_update";i:0;s:7:"contact";i:0;s:7:"context";i:0;s:18:"context_breadcrumb";i:0;s:10:"context_ui";i:0;s:10:"contextual";i:0;s:12:"core_context";i:0;s:4:"crop";i:0;s:17:"csv_serialization";i:0;s:6:"ctools";i:0;s:12:"ctools_block";i:0;s:9:"datalayer";i:0;s:8:"datetime";i:0;s:10:"dropzonejs";i:0;s:20:"dropzonejs_eb_widget";i:0;s:15:"dsu_c_accordion";i:0;s:26:"dsu_c_background_component";i:0;s:18:"dsu_c_contentcycle";i:0;s:17:"dsu_c_entitycycle";i:0;s:22:"dsu_c_extend_component";i:0;s:19:"dsu_c_externalvideo";i:0;s:13:"dsu_c_gallery";i:0;s:11:"dsu_c_image";i:0;s:29:"dsu_c_link_document_container";i:0;s:21:"dsu_c_product_extends";i:0;s:18:"dsu_c_sharebuttons";i:0;s:19:"dsu_c_sideimagetext";i:0;s:12:"dsu_c_slider";i:0;s:10:"dsu_c_text";i:0;s:15:"dsu_c_text_card";i:0;s:14:"dsu_c_text_dyk";i:0;s:10:"dsu_c_view";i:0;s:8:"dsu_core";i:0;s:19:"dsu_ratings_reviews";i:0;s:6:"editor";i:0;s:5:"embed";i:0;s:7:"encrypt";i:0;s:14:"entity_browser";i:0;s:22:"entity_class_formatter";i:0;s:12:"entity_clone";i:0;s:12:"entity_embed";i:0;s:26:"entity_reference_revisions";i:0;s:17:"external_hreflang";i:0;s:12:"externalauth";i:0;s:6:"facets";i:0;s:8:"features";i:0;s:11:"features_ui";i:0;s:5:"field";i:0;s:11:"field_group";i:0;s:11:"field_timer";i:0;s:8:"field_ui";i:0;s:4:"file";i:0;s:12:"file_browser";i:0;s:28:"file_upload_secure_validator";i:0;s:6:"filter";i:0;s:8:"fivestar";i:0;s:4:"flag";i:0;s:11:"focal_point";i:0;s:11:"geolocation";i:0;s:5:"gigya";i:0;s:10:"gigya_raas";i:0;s:19:"gigya_user_deletion";i:0;s:16:"google_analytics";i:0;s:15:"google_optimize";i:0;s:18:"google_optimize_js";i:0;s:10:"google_tag";i:0;s:8:"honeypot";i:0;s:5:"image";i:0;s:17:"image_widget_crop";i:0;s:18:"inline_entity_form";i:0;s:9:"jquery_ui";i:0;s:22:"jquery_ui_autocomplete";i:0;s:16:"jquery_ui_button";i:0;s:23:"jquery_ui_checkboxradio";i:0;s:22:"jquery_ui_controlgroup";i:0;s:20:"jquery_ui_datepicker";i:0;s:19:"jquery_ui_draggable";i:0;s:19:"jquery_ui_droppable";i:0;s:14:"jquery_ui_menu";i:0;s:17:"jquery_ui_spinner";i:0;s:3:"key";i:0;s:8:"language";i:0;s:14:"layout_builder";i:0;s:38:"layout_builder_expose_all_field_blocks";i:0;s:16:"layout_discovery";i:0;s:14:"layout_library";i:0;s:4:"link";i:0;s:15:"link_attributes";i:0;s:33:"link_attributes_menu_link_content";i:0;s:8:"ln_adimo";i:0;s:10:"ln_article";i:0;s:20:"ln_c_background_only";i:0;s:22:"ln_c_buy_now_component";i:0;s:9:"ln_c_card";i:0;s:13:"ln_c_cardgrid";i:0;s:11:"ln_c_spacer";i:0;s:11:"ln_fusepump";i:0;s:11:"ln_hreflang";i:0;s:9:"ln_m_core";i:0;s:13:"ln_m_document";i:0;s:19:"ln_m_external_video";i:0;s:9:"ln_n_core";i:0;s:15:"ln_price_spider";i:0;s:10:"ln_product";i:0;s:11:"ln_sso_saml";i:0;s:17:"ln_tint_connector";i:0;s:6:"locale";i:0;s:14:"login_security";i:0;s:5:"media";i:0;s:13:"media_library";i:0;s:8:"memcache";i:0;s:17:"menu_link_content";i:0;s:7:"menu_ui";i:0;s:7:"metatag";i:0;s:16:"metatag_hreflang";i:0;s:18:"metatag_open_graph";i:0;s:27:"metatag_open_graph_products";i:0;s:21:"metatag_twitter_cards";i:0;s:7:"migrate";i:0;s:30:"migrate_example_advanced_setup";i:0;s:21:"migrate_example_setup";i:0;s:12:"migrate_plus";i:0;s:18:"migrate_source_csv";i:0;s:13:"migrate_tools";i:0;s:10:"milo_covid";i:0;s:16:"milo_gtm_tagging";i:0;s:14:"milo_teen_quiz";i:0;s:18:"milo_vietnam_pages";i:0;s:5:"mysql";i:0;s:4:"node";i:0;s:10:"noreferrer";i:0;s:16:"ogilvy_c_article";i:0;s:18:"ogilvy_c_milestone";i:0;s:26:"ogilvy_c_sharebuttons_zalo";i:0;s:13:"ogilvy_c_text";i:0;s:9:"ogilvy_co";i:0;s:30:"ogilvy_co_article_listing_3in1";i:0;s:26:"ogilvy_co_bar_notification";i:0;s:29:"ogilvy_co_carousel_fulllength";i:0;s:28:"ogilvy_co_carousel_imageetch";i:0;s:27:"ogilvy_co_ciam_gigya_screen";i:0;s:25:"ogilvy_co_content_related";i:0;s:20:"ogilvy_co_error_page";i:0;s:23:"ogilvy_co_faq_accordion";i:0;s:24:"ogilvy_co_faq_search_bar";i:0;s:38:"ogilvy_co_infographic_nutritionalvalue";i:0;s:26:"ogilvy_co_intro_fulllength";i:0;s:24:"ogilvy_co_masthead_short";i:0;s:30:"ogilvy_co_nutritionalindicator";i:0;s:34:"ogilvy_co_nutritionalindicator_tab";i:0;s:25:"ogilvy_co_partnerslisting";i:0;s:32:"ogilvy_co_recipedescription_list";i:0;s:34:"ogilvy_co_recipedetails_headerdesc";i:0;s:38:"ogilvy_co_recipedetails_productmention";i:0;s:34:"ogilvy_co_recipedetails_stepbystep";i:0;s:32:"ogilvy_co_reviews_ratings_teaser";i:0;s:29:"ogilvy_co_section_teaser_card";i:0;s:22:"ogilvy_co_sportscentre";i:0;s:32:"ogilvy_co_sportslanding_masthead";i:0;s:14:"ogilvy_co_tint";i:0;s:35:"ogilvy_co_videolisting_1hero_3small";i:0;s:23:"ogilvy_co_view_selector";i:0;s:20:"ogilvy_co_winnerlist";i:0;s:13:"ogilvy_common";i:0;s:12:"ogilvy_gigya";i:0;s:20:"ogilvy_module_extend";i:0;s:29:"ogilvy_recipe_structured_data";i:0;s:13:"ogilvy_search";i:0;s:19:"ogilvy_sport_center";i:0;s:26:"ogilvy_template_suggestion";i:0;s:7:"options";i:0;s:13:"options_table";i:0;s:6:"panels";i:0;s:10:"panels_ipe";i:0;s:18:"paragraphs_browser";i:0;s:19:"paragraphs_features";i:0;s:20:"paragraphs_previewer";i:0;s:31:"password_policy_character_types";i:0;s:26:"password_policy_characters";i:0;s:23:"password_policy_history";i:0;s:22:"password_policy_length";i:0;s:24:"password_policy_username";i:0;s:4:"path";i:0;s:10:"path_alias";i:0;s:6:"phpass";i:0;s:15:"project_browser";i:0;s:8:"real_aes";i:0;s:9:"recaptcha";i:0;s:12:"recaptcha_v3";i:0;s:8:"redirect";i:0;s:16:"responsive_image";i:0;s:4:"rest";i:0;s:20:"restrict_route_by_ip";i:0;s:6:"restui";i:0;s:9:"robotstxt";i:0;s:8:"samlauth";i:0;s:19:"samlauth_user_roles";i:0;s:9:"scheduler";i:0;s:14:"schema_article";i:0;s:14:"schema_metatag";i:0;s:19:"schema_organization";i:0;s:14:"schema_product";i:0;s:13:"schema_recipe";i:0;s:15:"schema_web_site";i:0;s:10:"search_api";i:0;s:13:"search_api_db";i:0;s:6:"seckit";i:0;s:13:"serialization";i:0;s:13:"session_limit";i:0;s:6:"shield";i:0;s:8:"shortcut";i:0;s:5:"slick";i:0;s:21:"slick_entityreference";i:0;s:16:"slick_paragraphs";i:0;s:8:"slick_ui";i:0;s:12:"social_media";i:0;s:15:"svg_image_field";i:0;s:6:"syslog";i:0;s:6:"system";i:0;s:8:"taxonomy";i:0;s:4:"text";i:0;s:5:"token";i:0;s:7:"toolbar";i:0;s:10:"twig_tweak";i:0;s:4:"user";i:0;s:17:"video_embed_field";i:0;s:21:"views_infinite_scroll";i:0;s:8:"views_ui";i:0;s:14:"viewsreference";i:0;s:25:"vn_campaign_canister_2021";i:0;s:9:"votingapi";i:0;s:7:"webform";i:0;s:13:"webform_gigya";i:0;s:12:"webform_rest";i:0;s:23:"webform_scheduled_email";i:0;s:10:"webform_ui";i:0;s:13:"webform_views";i:0;s:17:"xls_serialization";i:0;s:2:"ds";i:1;s:10:"dsu_c_core";i:1;s:8:"pathauto";i:1;s:10:"xmlsitemap";i:1;s:15:"password_policy";i:10;s:5:"views";i:10;s:10:"paragraphs";i:11;s:15:"paragraphs_sets";i:12;s:13:"paragraphs_ee";i:15;s:12:"dsu_security";i:50;s:9:"lightnest";i:1000;s:3:"shs";i:1000;}s:5:"theme";a:5:{s:16:"bootstrap_barrio";i:0;s:5:"seven";i:0;s:6:"stable";i:0;s:4:"milo";i:0;s:5:"claro";i:0;}s:7:"profile";s:9:"lightnest";}'

# ── CLI ──────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    raw = sys.argv[1] if len(sys.argv) > 1 else PHP_STRING
    try:
        result = php_unserialize(raw)

        # Remove milo_teen_quiz from module dict
        if isinstance(result.get('module'), dict) and 'milo_teen_quiz' in result['module']:
            del result['module']['milo_teen_quiz']
            print("[removed] milo_teen_quiz from module", file=sys.stderr)

        serialized = php_serialize(result)
        print(serialized)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)