diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index 2c18715a..5a9babaf 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -68,6 +68,9 @@ int host_sdl_gl_swap_window(unsigned int window); int host_sdl_poll_event(void *event); int host_sdl_set_clipboard_text(const char *text); void host_sdl_get_clipboard_text(char *buffer, unsigned int size); +/* the keyboard's keys by name (the controls' bindings, xinput_sdl.c) */ +void host_sdl_scancode_name(int scancode, char *buffer, unsigned int size); +int host_sdl_scancode_from_name(const char *name); int host_sdl_show_toast(const char *message, int duration, int gravity, int x, int y); int host_sdl_show_simple_message_box(unsigned int flags, const char *title, const char *message); int host_sdl_get_gamepads(unsigned int *ids, int capacity); diff --git a/port/android/guest/runtime/guest_sdl.c b/port/android/guest/runtime/guest_sdl.c index ac775a6d..7bd89689 100644 --- a/port/android/guest/runtime/guest_sdl.c +++ b/port/android/guest/runtime/guest_sdl.c @@ -39,6 +39,19 @@ const char *SDL_GetError(void) return buffer; } +const char *SDL_GetScancodeName(SDL_Scancode scancode) +{ + static __thread char buffer[64]; + + host_sdl_scancode_name((int)scancode, buffer, sizeof(buffer)); + return buffer; +} + +SDL_Scancode SDL_GetScancodeFromName(const char *name) +{ + return (SDL_Scancode)host_sdl_scancode_from_name(name); +} + Uint64 SDL_GetTicks(void) { return (Uint64)host_sdl_ticks(); diff --git a/port/android/host/host_sdl.c b/port/android/host/host_sdl.c index 368705b2..0167e453 100644 --- a/port/android/host/host_sdl.c +++ b/port/android/host/host_sdl.c @@ -98,6 +98,16 @@ void host_sdl_get_error(char *buffer, uint32_t size) SDL_strlcpy(buffer, SDL_GetError(), size); } +void host_sdl_scancode_name(int32_t scancode, char *buffer, uint32_t size) +{ + SDL_strlcpy(buffer, SDL_GetScancodeName((SDL_Scancode)scancode), size); +} + +int32_t host_sdl_scancode_from_name(const char *name) +{ + return (int32_t)SDL_GetScancodeFromName(name); +} + int64_t host_sdl_ticks(void) { return (int64_t)SDL_GetTicks(); diff --git a/port/android/host_imports.list b/port/android/host_imports.list index 2888fe99..bb00cc89 100644 --- a/port/android/host_imports.list +++ b/port/android/host_imports.list @@ -38,6 +38,8 @@ host_sdl_gamepad_axis host_sdl_gamepad_button host_sdl_gamepad_type host_sdl_rumble_gamepad +host_sdl_scancode_name +host_sdl_scancode_from_name host_sdl_open_audio_stream host_sdl_put_audio_stream_data host_sdl_resume_audio_stream_device diff --git a/port/assets/menus/NON_HANDDRAWN.md b/port/assets/menus/NON_HANDDRAWN.md new file mode 100644 index 00000000..950737b1 --- /dev/null +++ b/port/assets/menus/NON_HANDDRAWN.md @@ -0,0 +1,57 @@ +# Menu pictures that are not redraws + +The menus' bitmaps are drawn from the high-res redraws (`svg/`, from ui-svg-handmade) where there is +one. The PC version's pictures that have none (3D renders, screenshots and logos) are not in this +repository. + +## From the Xbox's map + +These have the same pictures, in the same order, in the Xbox's `ui.map`, which the menus draw them +from (the player's own; nothing is shipped). `mp_map_grafix`: the Xbox's has the 13 Xbox maps' pictures +(the PC version's first 13) and its "?"; the PC version's other 8 are its own maps'. + +- `ui\shell\bitmaps\colors_sm` +- `ui\shell\bitmaps\mp_map_grafix` +- `ui\shell\bitmaps\sp_levels` +- `ui\shell\main_menu\difficulty_select\difficulty_options` +- `ui\shell\main_menu\difficulty_select\difficulty_options_small` +- `ui\shell\main_menu\halo_logo` +- `ui\shell\main_menu\settings_select\player_setup\player_profile_edit\color_edit\player_color_marine_large` + +These frames are drawn from a frame of the Xbox map's bitmap of the same name, whose other frames differ: +the same picture, or for the profile settings the Xbox's picture for the same thing. + +| The PC version's frame | The Xbox's frame | +| --- | --- | +| `shell/main_menu/multiplayer_type_select/mp_options__2.png` | `ui\shell\main_menu\multiplayer_type_select\mp_options` frame 3 | +| `shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__2.png` | `ui\shell\main_menu\settings_select\multiplayer_setup\playlist_edit\gametype_options` frame 2 | +| `shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__5.png` | `ui\shell\main_menu\settings_select\multiplayer_setup\playlist_edit\gametype_options` frame 4 | +| `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__0.png` | `ui\shell\main_menu\settings_select\player_setup\player_profile_edit\profile_options` frame 0 | +| `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__2.png` | `ui\shell\main_menu\settings_select\player_setup\player_profile_edit\profile_options` frame 1 | +| `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__7.png` | `ui\shell\main_menu\settings_select\player_setup\player_profile_edit\profile_options` frame 3 | + +## Placeholders, to be redrawn + +The Xbox's map has none of these (or other pictures under the name), so each frame is a placeholder +until it is redrawn. + +| File | Size | The PC version's picture | +| --- | --- | --- | +| `ce/shell/main_menu/multiplayer_type_select/mp_options__0.png` | 512x256 | `shell/main_menu/multiplayer_type_select/mp_options__0.png` | +| `ce/shell/main_menu/multiplayer_type_select/mp_options__1.png` | 512x256 | `shell/main_menu/multiplayer_type_select/mp_options__1.png` | +| `ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__0.png` | 512x256 | `shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__0.png` | +| `ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__3.png` | 512x256 | `shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__3.png` | +| `ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__4.png` | 512x256 | `shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__4.png` | +| `ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__6.png` | 512x256 | `shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__6.png` | +| `ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__7.png` | 512x256 | `shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__7.png` | +| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__1.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__1.png` | +| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__3.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__3.png` | +| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__4.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__4.png` | +| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__5.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__5.png` | +| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | +| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | + +13 of 147 frames are placeholders. + +The PC version's `ui\gamespy` and `ui\ticker` fonts are drawn with `ui\small_ui`, which +the Xbox's map has. diff --git a/port/assets/menus/README.md b/port/assets/menus/README.md new file mode 100644 index 00000000..c8ea5779 --- /dev/null +++ b/port/assets/menus/README.md @@ -0,0 +1,171 @@ +# Menus + +The game's menus are the PC version's (Halo Custom Edition's): its main menu +and every screen it leads to. They are XML files in this folder, which the +game builds into its own widget tags when the menus' map (`ui.map`) loads +(`port/linux/game/menu_tags.c`). They then work like the Xbox's menus: with a +controller, the keyboard and the mouse, on every port. + +| Path | Contents | +| --- | --- | +| `ce/*.xml` | The PC version's screens, one file for each folder of its tags, laid out as it has them; `ce/strings.xml` their text, `ce/bitmaps.xml` their pictures | +| `ce/shell/...png` | The pictures, drawn from the high-res redraws in `svg/` (up to 4x), else a placeholder | +| `NON_HANDDRAWN.md` | The pictures that are not redraws: those drawn from the Xbox's map (the player's own), and the placeholders, to be redrawn | +| `UNWIRED.md` | The menus' functions that do nothing yet: the lists they fill are empty, and the settings they change do not change | +| `menus.json` | The files the game embeds | + +`tools/ce_menus.py` writes all of them from the PC version's tags and the +redraws, but the settings screens (Controls Setup, Gamepads, Mouse, Audio, +Video and Network Setup), which `tools/port_settings.py` writes in their +style with what this port has to set: config.toml's settings (a window or +the full screen, the frame rate, the volumes, internet play, the +multiplayer HUD), the profile's controller settings, and the keyboard and +mouse's controls. + +To change the menus without building the game, put files in a `menus` folder +next to `config.toml`. A file with the same path as one here replaces it. On +the desktop, any other `.xml` file there is added too. A PNG that a `` +names is found there first. If any file has a problem, the log names the +file, the line and the problem, and the game uses the Xbox's menus. The +setting `display.menus = "xbox"` also uses them. `debug.menu_open` starts on +one screen (by its name, `main_menu/settings_select/...`), for looking at it. + +## Elements + +A file holds one `` element (`root` names the main menu's widget, in +one file; `main_menu` if none does). It holds ``, `` and +`` elements. All files share one set of names. Wherever a name is +asked for, one with a backslash is a tag of the map instead +(`ui\shell\bitmaps\white`). + +### `` + +```xml + + + + +``` + +A picture for widgets. Each `` is an 8-bit RGBA PNG and its size in +the 640x480 menu screen (the PNG is a whole multiple of it), or a frame of a +bitmap of the map: ``. (Or +`frames="a.png b.png"` with one `width` and `height` for all.) A widget draws +its bitmap from the top left, one unit to a texel, cut to the widget's size. +A list's item shows frame 1 while it has the focus and frame 0 otherwise, if +the bitmap has exactly two frames; the game's functions choose others. + +### `` + +```xml + + + + +``` + +A list of texts, which widgets show by number. `\n` starts a new line. + +### `` + +A screen, or a part of one. Its children are `` elements in it, or +`` for one defined elsewhere (a widget can +be a child of many). + +| Attribute | Value | +| --- | --- | +| `name` | Its name, unique (the game knows it as `pc\`, and shows the last part) | +| `type` | `container` (the default), `text`, `spinner` or `column_list` | +| `x`, `y` | Where it is in its parent (a `` inside another) | +| `left`, `top`, `width`, `height` | Its area; 0, 0, 640 and 480 if not given | +| `flags` | Widget flags (below), separated by spaces | +| `bitmap` | Its background: a `` | +| `string_list`, `string_index` | Its text: a `` and the number in it | +| `text` | Its text, given here | +| `font` | `large`, `small`, `terminal`, or a font tag of the map | +| `color` | The text's colour, `#RRGGBB` or `#AARRGGBB`. Focused text is white. | +| `align`, `text_x`, `text_y` | `left`, `right` or `center`, and where the text starts in it | +| `text_flags` | `editable`, `password`, `flashing`, `no_focus_test` | +| `list_flags` | `items_in_code`, `items_from_strings` (a spinner showing its strings), `one_tooltip`, `single_preview` | +| `strings`, `setting`, `values` | A spinner's values as shown, separated by `\|`; its `config.toml` setting and the values written for each (`port setting load` and `port setting save`) | +| `header_bitmap`, `footer_bitmap`, `header_bounds`, `footer_bounds` | A list's header and footer: a `` and "top left bottom right" | +| `description` | A column list's extended description: a widget | +| `controller` | `1` to `4`, or `any` (the default); a ``'s or `child_controller`: the one it is for | +| `auto_close`, `auto_close_fade` | It closes itself after this many milliseconds, fading for this many | +| `platform` | `desktop` or `android`: it is only there. Every element takes this. | + +Widget flags: `pass_unhandled_to_focused_child` (an event it does not handle +goes to its focused child: a screen needs it for its list to get events), +`pass_unhandled_to_all_children`, `pass_handled_to_all_children`, +`up_down_tabs_items`, `left_right_tabs_items`, `up_down_tabs_children`, +`left_right_tabs_children`, `no_focused_child`, `pause_game`, `flash_bitmap`, +`render_any_controller`, `main_menu_if_no_history`, `tag_controller_index`, +`nifty_fx`, `no_history`, and the PC version's `force_handle_mouse` and +`no_widescreen_fill`, which this engine has no use for. + +A widget takes the focus if it has an `` or is a list; a screen gives it +to its first such child. + +### `` + +What a widget does for an event: `event` (one or more, separated by spaces) +and what to do. + +| Attribute | Value | +| --- | --- | +| `event` | `a`, `b`, `x`, `y`, `black`, `white`, `left_trigger`, `right_trigger`, `up`, `down`, `left`, `right`, `start`, `back`, `left_thumb`, `right_thumb`, `stick_up`, `stick_down`, `stick_left`, `stick_right`, `right_stick_up`, `right_stick_down`, `right_stick_left`, `right_stick_right`, `created`, `deleted`; the PC version's `get_focus`, `lose_focus`, `left_mouse`, `middle_mouse`, `right_mouse`, `double_click`, `custom_activation` and `post_render`, which this engine never sends (but `custom_activation`: `emit custom activation event` and `single prev cl item activated` send it) | +| `run` | A function: the game's, as its tags name them; the PC version's; the port's (below); or `unwired `, which does nothing (`UNWIRED.md`) | +| `open`, `replace`, `focus` | A widget to open, to open in its place, to give the focus | +| `back` | `true`: back to the screen before | +| `close` | `current`, `all`, or `other` (with `widget`) | +| `reload` | `self`, or `other` (with `widget`) | +| `branch` | `true`: if `run` fails, open its `` widget | +| `otherwise` | A widget to open if `run` fails (a `` and `branch` in one) | +| `script` | A script of the menus' scenario to run | +| `label` | Kept as the script's name, not run (as the tags keep it) | +| `sound` | A sound tag of the map to play | + +On the keyboard, A is Enter or Space, B is Backspace or F, Start is Escape and +Back is F1. The mouse picks the item under it, clicks A, right-clicks B and +scrolls lists. + +The port's functions: `port quit game` quits; `port setting load` (on a +spinner's `created`) shows its setting's value and `port setting save` (on +its `deleted`) writes the value shown to `config.toml`, if it changed. Of +the PC version's: `main menu quit game` quits, `profile set edit begin` +begins editing the first player profile, `mouse emit back event` and +`mouse emit x event` push B and X, `emit custom activation event` and +`single prev cl item activated` run the screen's or the list's +`custom_activation` handlers, and its back handlers go back. Its +campaign's play: `campaign menu continue` (the saved game), New Game's list +of levels and the difficulty menu (`difficulty item select`, and `set +difficulty` on its OK button: the difficulty shown), and Load Game's list of +the profiles' saved games (with `load game menu delete finish`). For our +widgets, `initialize sp level list solo`, `solo level set map` and `set +difficulty` are the port's, as the Xbox's take the Xbox's widgets. The +settings screens': `port settings save` (OK) writes the settings changed +(config.toml's, or `profile.`: the profile being edited) and applies +them, `port settings defaults` shows the defaults, `port settings help` the +help of the row chosen; Controls Setup's `controls ...` bind the keyboard +and mouse's controls (Enter takes the next key or button pressed; left and +right choose which of two; Escape cancels, Delete clears); Change Color's +`color picker ...` (the Xbox's names) set the profile's colour. The others +do nothing yet. + +### ``, ``, `` + +``: a game data function, which fills in the widget every +frame (`build number textbox only`). ``: a widget for a `branch` handler. ``: text replaced in the widget's (the controller's +number). + +## The art + +`tools/ce_menus.py` draws each frame from its redraw in `svg/` (copied from +ui-svg-handmade) at up to 4x its size. A picture with no redraw is the +Xbox's map's, where it has the same one (a whole bitmap, or a ``), else a placeholder, placed where the PC version's picture is in +its frame (`--pictures`: only where, not the picture, is read); +`NON_HANDDRAWN.md` lists both. It needs `rsvg-convert`, Pillow, NumPy and +SciPy. diff --git a/port/assets/menus/UNWIRED.md b/port/assets/menus/UNWIRED.md new file mode 100644 index 00000000..067d67cf --- /dev/null +++ b/port/assets/menus/UNWIRED.md @@ -0,0 +1,37 @@ +# Menu functions that are not wired yet + +The PC version's menus (`ce/`, from `tools/ce_menus.py`) run functions of the game's. These do nothing +yet (`port/linux/game/menu_functions.c`), so what they fill in (lists, values) is empty, and what they +do (save a setting, join a game) does not happen; the screens open and close as the PC version's. + +- The PC version's own functions, which the Xbox's game has not. +- The Xbox's functions that the PC version rewrote for its own widgets, written `unwired ` in the + files: run on the PC widgets, the Xbox's would stop the game. + +| Function | Uses | | +| --- | --- | --- | +| `common button bar update` | 24 | PC function | +| `direct ip connect init` | 1 | PC function | +| `direct ip connect update` | 1 | PC function | +| `direct ip edit field` | 4 | PC function | +| `gamespy screen dispose` | 1 | PC function | +| `gamespy screen update` | 1 | PC function | +| `gamespy select button` | 8 | PC function | +| `gamespy select header` | 22 | PC function | +| `gamespy select item` | 49 | PC function | +| `gamespy update filter settings` | 2 | PC function | +| `gt edit list update` | 1 | PC function | +| `gt select list update` | 1 | PC function | +| `mouse spinner 1wide click` | 64 | PC function | +| `mp map list update` | 1 | PC function | +| `mp prof init teamplay options` | 1 | PC function | +| `mp prof init vehicle options` | 1 | PC function | +| `mp prof save teamplay options` | 1 | PC function | +| `mp prof save vehicle options` | 1 | PC function | +| `mp prof vehicles update` | 1 | PC function | +| `server settings init` | 1 | PC function | +| `server settings update` | 1 | PC function | +| `ss edit server name` | 2 | PC function | +| `ss start game` | 2 | PC function | + +Wired: `campaign menu continue`, `campaign menu init`, `controls back handler`, `controls begin binding`, `controls screen change set`, `controls screen defaults`, `controls screen init`, `controls update menu`, `difficulty item select`, `direct ip connect go`, `emit custom activation event`, `gamespy back handler`, `gamespy dismiss error`, `gamespy dismiss filters`, `gamespy screen init`, `load game list update`, `load game menu activated`, `load game menu delete finish`, `load game menu delete request`, `load game menu dispose`, `load game menu init`, `main menu quit game`, `mouse emit accept event`, `mouse emit back event`, `mouse emit x event`, `mp type set mode`, `profile manager select`, `profile set edit begin`, `single prev cl item activated`, `solo map list update`. diff --git a/port/assets/menus/ce/bitmaps.xml b/port/assets/menus/ce/bitmaps.xml new file mode 100644 index 00000000..2a4a6f5a --- /dev/null +++ b/port/assets/menus/ce/bitmaps.xml @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/error.xml b/port/assets/menus/ce/error.xml new file mode 100644 index 00000000..fd7b1d2a --- /dev/null +++ b/port/assets/menus/ce/error.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.campaign_select.xml b/port/assets/menus/ce/main_menu.campaign_select.xml new file mode 100644 index 00000000..4273eaa1 --- /dev/null +++ b/port/assets/menus/ce/main_menu.campaign_select.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.difficulty_select.xml b/port/assets/menus/ce/main_menu.difficulty_select.xml new file mode 100644 index 00000000..2fbe30e4 --- /dev/null +++ b/port/assets/menus/ce/main_menu.difficulty_select.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.gametype_select.xml b/port/assets/menus/ce/main_menu.gametype_select.xml new file mode 100644 index 00000000..937f3741 --- /dev/null +++ b/port/assets/menus/ce/main_menu.gametype_select.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.connected.4way_profile_select.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.connected.4way_profile_select.xml new file mode 100644 index 00000000..29248536 --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.connected.4way_profile_select.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.connected.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.connected.xml new file mode 100644 index 00000000..786f8ca1 --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.connected.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.direct_ip.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.direct_ip.xml new file mode 100644 index 00000000..5e8e83d7 --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.direct_ip.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.filters.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.filters.xml new file mode 100644 index 00000000..004e9ab2 --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.filters.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.port.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.port.xml new file mode 100644 index 00000000..8a46a74d --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.port.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.xml new file mode 100644 index 00000000..72b23ccb --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.join_game.xml @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml new file mode 100644 index 00000000..28338f3a --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.mp_map_select.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.mp_map_select.xml new file mode 100644 index 00000000..3d822835 --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.mp_map_select.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml new file mode 100644 index 00000000..ca45d7e0 --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.xml new file mode 100644 index 00000000..e417fccc --- /dev/null +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.new_select.xml b/port/assets/menus/ce/main_menu.new_select.xml new file mode 100644 index 00000000..9d81550e --- /dev/null +++ b/port/assets/menus/ce/main_menu.new_select.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.player_profiles_select.xml b/port/assets/menus/ce/main_menu.player_profiles_select.xml new file mode 100644 index 00000000..6974c020 --- /dev/null +++ b/port/assets/menus/ce/main_menu.player_profiles_select.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.profile_manager.xml b/port/assets/menus/ce/main_menu.profile_manager.xml new file mode 100644 index 00000000..0ae966ef --- /dev/null +++ b/port/assets/menus/ce/main_menu.profile_manager.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.quit_select.xml b/port/assets/menus/ce/main_menu.quit_select.xml new file mode 100644 index 00000000..9fd23989 --- /dev/null +++ b/port/assets/menus/ce/main_menu.quit_select.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.indicator_options_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.indicator_options_edit.xml new file mode 100644 index 00000000..2b2b9eef --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.indicator_options_edit.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.item_options_edit.port.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.item_options_edit.port.xml new file mode 100644 index 00000000..fd7075e8 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.item_options_edit.port.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.item_options_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.item_options_edit.xml new file mode 100644 index 00000000..5d8d1802 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.item_options_edit.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.name_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.name_edit.xml new file mode 100644 index 00000000..58312549 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.name_edit.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.player_options_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.player_options_edit.xml new file mode 100644 index 00000000..363b4c56 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.player_options_edit.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.ctf_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.ctf_edit.xml new file mode 100644 index 00000000..332ecc2f --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.ctf_edit.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.game_type_select.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.game_type_select.xml new file mode 100644 index 00000000..e0c90d74 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.game_type_select.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.koth_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.koth_edit.xml new file mode 100644 index 00000000..95c8d373 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.koth_edit.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.oddball_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.oddball_edit.xml new file mode 100644 index 00000000..83aed489 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.oddball_edit.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.race_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.race_edit.xml new file mode 100644 index 00000000..abfb1429 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.race_edit.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.slayer_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.slayer_edit.xml new file mode 100644 index 00000000..30da0efc --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.slayer_edit.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.xml new file mode 100644 index 00000000..d168333d --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_edit.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_select.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_select.xml new file mode 100644 index 00000000..188ab459 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.playlist_select.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml new file mode 100644 index 00000000..898b5c7e --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.vehicle_options_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.vehicle_options_edit.xml new file mode 100644 index 00000000..d7f89beb --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.vehicle_options_edit.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.xml new file mode 100644 index 00000000..0e16418d --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml new file mode 100644 index 00000000..23bc0fb6 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.color_edit.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.color_edit.xml new file mode 100644 index 00000000..e4b6c362 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.color_edit.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.controls_setup.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.controls_setup.xml new file mode 100644 index 00000000..9d3ef2af --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.controls_setup.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.gamepad_setup.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.gamepad_setup.xml new file mode 100644 index 00000000..c6049587 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.gamepad_setup.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml new file mode 100644 index 00000000..a26889d4 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.name_edit.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.name_edit.xml new file mode 100644 index 00000000..46ec7cc5 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.name_edit.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.xml new file mode 100644 index 00000000..c5677d85 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml new file mode 100644 index 00000000..b66f0660 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.xml new file mode 100644 index 00000000..377fc368 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.solo_level_select.xml b/port/assets/menus/ce/main_menu.solo_level_select.xml new file mode 100644 index 00000000..3da98277 --- /dev/null +++ b/port/assets/menus/ce/main_menu.solo_level_select.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.xml b/port/assets/menus/ce/main_menu.xml new file mode 100644 index 00000000..63953084 --- /dev/null +++ b/port/assets/menus/ce/main_menu.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png new file mode 100644 index 00000000..b601e0b4 Binary files /dev/null and b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png differ diff --git a/port/assets/menus/ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png new file mode 100644 index 00000000..226e2d88 Binary files /dev/null and b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png differ diff --git a/port/assets/menus/ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png new file mode 100644 index 00000000..3c7ba641 Binary files /dev/null and b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png differ diff --git a/port/assets/menus/ce/shell.xml b/port/assets/menus/ce/shell.xml new file mode 100644 index 00000000..2e13c880 --- /dev/null +++ b/port/assets/menus/ce/shell.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/shell/bitmaps/alert_bkd__0.png b/port/assets/menus/ce/shell/bitmaps/alert_bkd__0.png new file mode 100644 index 00000000..9a93ead1 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/alert_bkd__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_down__0.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_down__0.png new file mode 100644 index 00000000..c6ac1c12 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_down__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_down__1.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_down__1.png new file mode 100644 index 00000000..06b31117 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_down__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__0.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__0.png new file mode 100644 index 00000000..7a697aa5 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__1.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__1.png new file mode 100644 index 00000000..12284950 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__2.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__2.png new file mode 100644 index 00000000..828697f5 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__2.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__3.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__3.png new file mode 100644 index 00000000..c990429a Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_left__3.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__0.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__0.png new file mode 100644 index 00000000..684e3d45 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__1.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__1.png new file mode 100644 index 00000000..559215e1 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__2.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__2.png new file mode 100644 index 00000000..f3ffa2ab Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__2.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__3.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__3.png new file mode 100644 index 00000000..e28585e3 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_right__3.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_up__0.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_up__0.png new file mode 100644 index 00000000..5eaac1f0 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_up__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/arrow_sm_up__1.png b/port/assets/menus/ce/shell/bitmaps/arrow_sm_up__1.png new file mode 100644 index 00000000..efa6f317 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/arrow_sm_up__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/black__0.png b/port/assets/menus/ce/shell/bitmaps/black__0.png new file mode 100644 index 00000000..dee27edc Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/black__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/blue__0.png b/port/assets/menus/ce/shell/bitmaps/blue__0.png new file mode 100644 index 00000000..ebe16989 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/blue__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/controller_border__0.png b/port/assets/menus/ce/shell/bitmaps/controller_border__0.png new file mode 100644 index 00000000..2b2f45e1 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/controller_border__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/device_border__0.png b/port/assets/menus/ce/shell/bitmaps/device_border__0.png new file mode 100644 index 00000000..581a193a Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/device_border__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/game_type_grafix__0.png b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__0.png new file mode 100644 index 00000000..a9aa3c45 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/game_type_grafix__1.png b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__1.png new file mode 100644 index 00000000..456aa48e Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/game_type_grafix__2.png b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__2.png new file mode 100644 index 00000000..5c1f8435 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__2.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/game_type_grafix__3.png b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__3.png new file mode 100644 index 00000000..87b25bed Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__3.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/game_type_grafix__4.png b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__4.png new file mode 100644 index 00000000..2a10a5a0 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__4.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/game_type_grafix__5.png b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__5.png new file mode 100644 index 00000000..f35bee3c Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/game_type_grafix__5.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/gradient__0.png b/port/assets/menus/ce/shell/bitmaps/gradient__0.png new file mode 100644 index 00000000..509b951f Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/gradient__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/gradient_big__0.png b/port/assets/menus/ce/shell/bitmaps/gradient_big__0.png new file mode 100644 index 00000000..5b704303 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/gradient_big__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/join_game_border__0.png b/port/assets/menus/ce/shell/bitmaps/join_game_border__0.png new file mode 100644 index 00000000..f82e1ff6 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/join_game_border__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/list_field__0.png b/port/assets/menus/ce/shell/bitmaps/list_field__0.png new file mode 100644 index 00000000..d1da6479 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/list_field__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/list_item_bkd__0.png b/port/assets/menus/ce/shell/bitmaps/list_item_bkd__0.png new file mode 100644 index 00000000..8b84aed2 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/list_item_bkd__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/list_item_bkd__1.png b/port/assets/menus/ce/shell/bitmaps/list_item_bkd__1.png new file mode 100644 index 00000000..6be327fd Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/list_item_bkd__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/list_item_bkd_top__0.png b/port/assets/menus/ce/shell/bitmaps/list_item_bkd_top__0.png new file mode 100644 index 00000000..8b84aed2 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/list_item_bkd_top__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/list_item_bkd_top__1.png b/port/assets/menus/ce/shell/bitmaps/list_item_bkd_top__1.png new file mode 100644 index 00000000..d48f2c3a Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/list_item_bkd_top__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/locked_gametype__0.png b/port/assets/menus/ce/shell/bitmaps/locked_gametype__0.png new file mode 100644 index 00000000..c5e71649 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/locked_gametype__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/locked_gametype__1.png b/port/assets/menus/ce/shell/bitmaps/locked_gametype__1.png new file mode 100644 index 00000000..da04edbd Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/locked_gametype__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/option_bkds__0.png b/port/assets/menus/ce/shell/bitmaps/option_bkds__0.png new file mode 100644 index 00000000..06303db9 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/option_bkds__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/option_bkds__1.png b/port/assets/menus/ce/shell/bitmaps/option_bkds__1.png new file mode 100644 index 00000000..a3226449 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/option_bkds__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/option_bkds_sm2__0.png b/port/assets/menus/ce/shell/bitmaps/option_bkds_sm2__0.png new file mode 100644 index 00000000..22f22f88 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/option_bkds_sm2__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/option_bkds_sm2__1.png b/port/assets/menus/ce/shell/bitmaps/option_bkds_sm2__1.png new file mode 100644 index 00000000..c9ebeaf4 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/option_bkds_sm2__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/option_bkds_sm__0.png b/port/assets/menus/ce/shell/bitmaps/option_bkds_sm__0.png new file mode 100644 index 00000000..e000584b Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/option_bkds_sm__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/option_bkds_sm__1.png b/port/assets/menus/ce/shell/bitmaps/option_bkds_sm__1.png new file mode 100644 index 00000000..152b9ea6 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/option_bkds_sm__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/sel_list_desc_bkd__0.png b/port/assets/menus/ce/shell/bitmaps/sel_list_desc_bkd__0.png new file mode 100644 index 00000000..f7ae2436 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/sel_list_desc_bkd__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__0.png b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__0.png new file mode 100644 index 00000000..8b84aed2 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__1.png b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__1.png new file mode 100644 index 00000000..faeb7b6d Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__2.png b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__2.png new file mode 100644 index 00000000..39b3ab7d Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd__2.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__0.png b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__0.png new file mode 100644 index 00000000..8b84aed2 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__1.png b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__1.png new file mode 100644 index 00000000..39a78209 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__2.png b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__2.png new file mode 100644 index 00000000..39b3ab7d Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/sel_list_item_bkd_top__2.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/semi_transparent_grey__0.png b/port/assets/menus/ce/shell/bitmaps/semi_transparent_grey__0.png new file mode 100644 index 00000000..0fa99c2b Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/semi_transparent_grey__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_header_background_0__0.png b/port/assets/menus/ce/shell/bitmaps/server_header_background_0__0.png new file mode 100644 index 00000000..a752ae1e Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_header_background_0__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_header_background_0__1.png b/port/assets/menus/ce/shell/bitmaps/server_header_background_0__1.png new file mode 100644 index 00000000..d731fd34 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_header_background_0__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_header_background_1__0.png b/port/assets/menus/ce/shell/bitmaps/server_header_background_1__0.png new file mode 100644 index 00000000..164a0ff4 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_header_background_1__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_header_background_1__1.png b/port/assets/menus/ce/shell/bitmaps/server_header_background_1__1.png new file mode 100644 index 00000000..628b2f93 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_header_background_1__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_header_background_2__0.png b/port/assets/menus/ce/shell/bitmaps/server_header_background_2__0.png new file mode 100644 index 00000000..2c2507c0 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_header_background_2__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_header_background_2__1.png b/port/assets/menus/ce/shell/bitmaps/server_header_background_2__1.png new file mode 100644 index 00000000..b0ef5020 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_header_background_2__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_header_background_3__0.png b/port/assets/menus/ce/shell/bitmaps/server_header_background_3__0.png new file mode 100644 index 00000000..4845c522 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_header_background_3__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_header_background_3__1.png b/port/assets/menus/ce/shell/bitmaps/server_header_background_3__1.png new file mode 100644 index 00000000..788de8f7 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_header_background_3__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_item_bkds__0.png b/port/assets/menus/ce/shell/bitmaps/server_item_bkds__0.png new file mode 100644 index 00000000..c5d0b1d2 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_item_bkds__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_item_bkds__1.png b/port/assets/menus/ce/shell/bitmaps/server_item_bkds__1.png new file mode 100644 index 00000000..993e37a1 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_item_bkds__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/server_item_bkds__2.png b/port/assets/menus/ce/shell/bitmaps/server_item_bkds__2.png new file mode 100644 index 00000000..4b71812a Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/server_item_bkds__2.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/spinner_list_3_wide_item_background__0.png b/port/assets/menus/ce/shell/bitmaps/spinner_list_3_wide_item_background__0.png new file mode 100644 index 00000000..94244ae1 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/spinner_list_3_wide_item_background__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/spinner_list_3_wide_item_background__1.png b/port/assets/menus/ce/shell/bitmaps/spinner_list_3_wide_item_background__1.png new file mode 100644 index 00000000..f8555c3e Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/spinner_list_3_wide_item_background__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/text_button_background__0.png b/port/assets/menus/ce/shell/bitmaps/text_button_background__0.png new file mode 100644 index 00000000..e1beb4a4 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/text_button_background__0.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/text_button_background__1.png b/port/assets/menus/ce/shell/bitmaps/text_button_background__1.png new file mode 100644 index 00000000..c8de42dc Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/text_button_background__1.png differ diff --git a/port/assets/menus/ce/shell/bitmaps/white__0.png b/port/assets/menus/ce/shell/bitmaps/white__0.png new file mode 100644 index 00000000..3aae5b17 Binary files /dev/null and b/port/assets/menus/ce/shell/bitmaps/white__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/header_load_game__0.png b/port/assets/menus/ce/shell/main_menu/campaign_select/header_load_game__0.png new file mode 100644 index 00000000..cb3854bd Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/header_load_game__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/item_back__0.png b/port/assets/menus/ce/shell/main_menu/campaign_select/item_back__0.png new file mode 100644 index 00000000..4194892d Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/item_back__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/item_back__1.png b/port/assets/menus/ce/shell/main_menu/campaign_select/item_back__1.png new file mode 100644 index 00000000..ab5ab389 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/item_back__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/item_continue__0.png b/port/assets/menus/ce/shell/main_menu/campaign_select/item_continue__0.png new file mode 100644 index 00000000..3eacdb1e Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/item_continue__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/item_continue__1.png b/port/assets/menus/ce/shell/main_menu/campaign_select/item_continue__1.png new file mode 100644 index 00000000..b622ebf0 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/item_continue__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/item_load_game__0.png b/port/assets/menus/ce/shell/main_menu/campaign_select/item_load_game__0.png new file mode 100644 index 00000000..bcefa8ca Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/item_load_game__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/item_load_game__1.png b/port/assets/menus/ce/shell/main_menu/campaign_select/item_load_game__1.png new file mode 100644 index 00000000..8c56e555 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/item_load_game__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/item_new_campaign__0.png b/port/assets/menus/ce/shell/main_menu/campaign_select/item_new_campaign__0.png new file mode 100644 index 00000000..22156bb4 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/item_new_campaign__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/campaign_select/item_new_campaign__1.png b/port/assets/menus/ce/shell/main_menu/campaign_select/item_new_campaign__1.png new file mode 100644 index 00000000..18754cf5 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/campaign_select/item_new_campaign__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/difficulty_select/header_choose_difficulty__0.png b/port/assets/menus/ce/shell/main_menu/difficulty_select/header_choose_difficulty__0.png new file mode 100644 index 00000000..df3a5b4f Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/difficulty_select/header_choose_difficulty__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/gametype_select/header_select_gametype__0.png b/port/assets/menus/ce/shell/main_menu/gametype_select/header_select_gametype__0.png new file mode 100644 index 00000000..6008f5fe Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/gametype_select/header_select_gametype__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_load_campaign__0.png b/port/assets/menus/ce/shell/main_menu/menu_load_campaign__0.png new file mode 100644 index 00000000..9e7a272a Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_load_campaign__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_load_campaign__1.png b/port/assets/menus/ce/shell/main_menu/menu_load_campaign__1.png new file mode 100644 index 00000000..d062fc54 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_load_campaign__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_multiplayer__0.png b/port/assets/menus/ce/shell/main_menu/menu_multiplayer__0.png new file mode 100644 index 00000000..cb28e179 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_multiplayer__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_multiplayer__1.png b/port/assets/menus/ce/shell/main_menu/menu_multiplayer__1.png new file mode 100644 index 00000000..48abb1de Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_multiplayer__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_profile__0.png b/port/assets/menus/ce/shell/main_menu/menu_profile__0.png new file mode 100644 index 00000000..0c1f1c19 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_profile__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_profile__1.png b/port/assets/menus/ce/shell/main_menu/menu_profile__1.png new file mode 100644 index 00000000..d6916527 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_profile__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_quit__0.png b/port/assets/menus/ce/shell/main_menu/menu_quit__0.png new file mode 100644 index 00000000..30cd1763 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_quit__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_quit__1.png b/port/assets/menus/ce/shell/main_menu/menu_quit__1.png new file mode 100644 index 00000000..174a9cfa Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_quit__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_settings__0.png b/port/assets/menus/ce/shell/main_menu/menu_settings__0.png new file mode 100644 index 00000000..1369eaba Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_settings__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/menu_settings__1.png b/port/assets/menus/ce/shell/main_menu/menu_settings__1.png new file mode 100644 index 00000000..0f72448b Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/menu_settings__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/direct_ip/header_direct_ip__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/direct_ip/header_direct_ip__0.png new file mode 100644 index 00000000..83988b8e Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/direct_ip/header_direct_ip__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/header_multiplayer__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/header_multiplayer__0.png new file mode 100644 index 00000000..65b6dade Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/header_multiplayer__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__0.png new file mode 100644 index 00000000..da04edbd Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__1.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__1.png new file mode 100644 index 00000000..c6ac5f68 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__2.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__2.png new file mode 100644 index 00000000..d35c34b8 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__2.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__3.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__3.png new file mode 100644 index 00000000..b56faa8d Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__3.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__0.png new file mode 100644 index 00000000..da04edbd Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__1.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__1.png new file mode 100644 index 00000000..0a9c1b07 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__2.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__2.png new file mode 100644 index 00000000..8a1137a0 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__2.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__3.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__3.png new file mode 100644 index 00000000..7432a202 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__3.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/filters/header_filters__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/filters/header_filters__0.png new file mode 100644 index 00000000..73e8830a Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/filters/header_filters__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/header_internet__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/header_internet__0.png new file mode 100644 index 00000000..67b29197 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/header_internet__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/header_lan__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/header_lan__0.png new file mode 100644 index 00000000..89046a49 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/header_lan__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__0.png new file mode 100644 index 00000000..da04edbd Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__1.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__1.png new file mode 100644 index 00000000..9e0f2e0a Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__2.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__2.png new file mode 100644 index 00000000..ffb41be4 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__2.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__3.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__3.png new file mode 100644 index 00000000..4eb37719 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__3.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_map_select/header_select_map__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_map_select/header_select_map__0.png new file mode 100644 index 00000000..471b15b1 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_map_select/header_select_map__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_options__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_options__0.png new file mode 100644 index 00000000..092acd71 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_options__1.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_options__1.png new file mode 100644 index 00000000..7dde9ed9 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/mp_options__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/server_settings/header_server_settings__0.png b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/server_settings/header_server_settings__0.png new file mode 100644 index 00000000..cd709b4e Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/multiplayer_type_select/server_settings/header_server_settings__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/player_profiles_select/header_select_profile__0.png b/port/assets/menus/ce/shell/main_menu/player_profiles_select/header_select_profile__0.png new file mode 100644 index 00000000..d6302dfe Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/player_profiles_select/header_select_profile__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/indicator_options_edit/header_indicator_options__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/indicator_options_edit/header_indicator_options__0.png new file mode 100644 index 00000000..9d1c1c07 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/indicator_options_edit/header_indicator_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/item_options_edit/header_item_options__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/item_options_edit/header_item_options__0.png new file mode 100644 index 00000000..683b4056 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/item_options_edit/header_item_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/name_edit/header_gametype_name__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/name_edit/header_gametype_name__0.png new file mode 100644 index 00000000..ce2ab40b Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/name_edit/header_gametype_name__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/player_options_edit/header_player_options__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/player_options_edit/header_player_options__0.png new file mode 100644 index 00000000..7b87412c Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/player_options_edit/header_player_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/header_rules__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/header_rules__0.png new file mode 100644 index 00000000..845d73f3 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/header_rules__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__0.png new file mode 100644 index 00000000..62019a0b Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__1.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__1.png new file mode 100644 index 00000000..67db5f36 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__2.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__2.png new file mode 100644 index 00000000..d09ffc96 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__2.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__3.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__3.png new file mode 100644 index 00000000..c98b9235 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__3.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__4.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__4.png new file mode 100644 index 00000000..be99ff53 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__4.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__5.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__5.png new file mode 100644 index 00000000..2171ef41 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__5.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__0.png new file mode 100644 index 00000000..40de78c2 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__1.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__1.png new file mode 100644 index 00000000..b527a33a Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__3.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__3.png new file mode 100644 index 00000000..eac24a93 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__3.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__4.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__4.png new file mode 100644 index 00000000..bd6a94a6 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__4.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__6.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__6.png new file mode 100644 index 00000000..0ca56445 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__6.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__7.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__7.png new file mode 100644 index 00000000..653c46a6 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__7.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_edit_multiplayer_gametype__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_edit_multiplayer_gametype__0.png new file mode 100644 index 00000000..a41bbc8b Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_edit_multiplayer_gametype__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_set_options__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_set_options__0.png new file mode 100644 index 00000000..5df4838d Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_set_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_select/header_select_gametype_to_edit__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_select/header_select_gametype_to_edit__0.png new file mode 100644 index 00000000..26733907 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/playlist_select/header_select_gametype_to_edit__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/teamplay_options_edit/header_teamplay_options__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/teamplay_options_edit/header_teamplay_options__0.png new file mode 100644 index 00000000..352755c1 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/teamplay_options_edit/header_teamplay_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/vehicle_options_edit/header_vehicle_options__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/vehicle_options_edit/header_vehicle_options__0.png new file mode 100644 index 00000000..60906488 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/multiplayer_setup/vehicle_options_edit/header_vehicle_options__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/audio_settings/header_profile_audio_settings__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/audio_settings/header_profile_audio_settings__0.png new file mode 100644 index 00000000..54f988e6 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/audio_settings/header_profile_audio_settings__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/color_edit/header_profile_color__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/color_edit/header_profile_color__0.png new file mode 100644 index 00000000..a1588141 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/color_edit/header_profile_color__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/controller_edit/header_profile_controller_setting__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/controller_edit/header_profile_controller_setting__0.png new file mode 100644 index 00000000..cd7a6b85 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/controller_edit/header_profile_controller_setting__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/controls_setup/header_profile_controls_settings__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/controls_setup/header_profile_controls_settings__0.png new file mode 100644 index 00000000..3a786c55 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/controls_setup/header_profile_controls_settings__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/header_edit_profile_settings__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/header_edit_profile_settings__0.png new file mode 100644 index 00000000..d82ab362 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/header_edit_profile_settings__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/header_profile_mouse_settings__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/header_profile_mouse_settings__0.png new file mode 100644 index 00000000..9900a841 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/header_profile_mouse_settings__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/name_edit/header_profile_name__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/name_edit/header_profile_name__0.png new file mode 100644 index 00000000..fb2a4118 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/name_edit/header_profile_name__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings__0.png new file mode 100644 index 00000000..ccb78499 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__1.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__1.png new file mode 100644 index 00000000..f9338a3f Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__1.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__3.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__3.png new file mode 100644 index 00000000..2bf9b958 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__3.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__4.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__4.png new file mode 100644 index 00000000..a4c47043 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__4.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__5.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__5.png new file mode 100644 index 00000000..7dde9ed9 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__5.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png new file mode 100644 index 00000000..4327cac4 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png new file mode 100644 index 00000000..17e0825f Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png differ diff --git a/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings__0.png b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings__0.png new file mode 100644 index 00000000..7e8c824a Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings__0.png differ diff --git a/port/assets/menus/ce/shell/main_menu/solo_level_select/header_load_level__0.png b/port/assets/menus/ce/shell/main_menu/solo_level_select/header_load_level__0.png new file mode 100644 index 00000000..1237e312 Binary files /dev/null and b/port/assets/menus/ce/shell/main_menu/solo_level_select/header_load_level__0.png differ diff --git a/port/assets/menus/ce/strings.xml b/port/assets/menus/ce/strings.xml new file mode 100644 index 00000000..2730389b --- /dev/null +++ b/port/assets/menus/ce/strings.xml @@ -0,0 +1,1473 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json new file mode 100644 index 00000000..6dc6e4e3 --- /dev/null +++ b/port/assets/menus/menus.json @@ -0,0 +1,200 @@ +{ + "comment": "The menus' files the game embeds (tools/ce_menus.py, tools/embed_assets.py).", + "files": [ + "ce/bitmaps.xml", + "ce/error.xml", + "ce/main_menu.campaign_select.xml", + "ce/main_menu.difficulty_select.xml", + "ce/main_menu.gametype_select.xml", + "ce/main_menu.multiplayer_type_select.connected.4way_profile_select.xml", + "ce/main_menu.multiplayer_type_select.connected.xml", + "ce/main_menu.multiplayer_type_select.direct_ip.xml", + "ce/main_menu.multiplayer_type_select.join_game.filters.xml", + "ce/main_menu.multiplayer_type_select.join_game.port.xml", + "ce/main_menu.multiplayer_type_select.join_game.xml", + "ce/main_menu.multiplayer_type_select.lobby.xml", + "ce/main_menu.multiplayer_type_select.mp_map_select.xml", + "ce/main_menu.multiplayer_type_select.server_settings.xml", + "ce/main_menu.multiplayer_type_select.xml", + "ce/main_menu.new_select.xml", + "ce/main_menu.player_profiles_select.xml", + "ce/main_menu.profile_manager.xml", + "ce/main_menu.quit_select.xml", + "ce/main_menu.settings_select.multiplayer_setup.indicator_options_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.item_options_edit.port.xml", + "ce/main_menu.settings_select.multiplayer_setup.item_options_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.name_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.player_options_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.ctf_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.game_type_select.xml", + "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.koth_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.oddball_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.race_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.slayer_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.playlist_select.xml", + "ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.vehicle_options_edit.xml", + "ce/main_menu.settings_select.multiplayer_setup.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.color_edit.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.controls_setup.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.gamepad_setup.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.name_edit.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.xml", + "ce/main_menu.solo_level_select.xml", + "ce/main_menu.xml", + "ce/shell.xml", + "ce/strings.xml", + "ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png", + "ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png", + "ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png", + "ce/shell/bitmaps/alert_bkd__0.png", + "ce/shell/bitmaps/arrow_sm_down__0.png", + "ce/shell/bitmaps/arrow_sm_down__1.png", + "ce/shell/bitmaps/arrow_sm_left__0.png", + "ce/shell/bitmaps/arrow_sm_left__1.png", + "ce/shell/bitmaps/arrow_sm_left__2.png", + "ce/shell/bitmaps/arrow_sm_left__3.png", + "ce/shell/bitmaps/arrow_sm_right__0.png", + "ce/shell/bitmaps/arrow_sm_right__1.png", + "ce/shell/bitmaps/arrow_sm_right__2.png", + "ce/shell/bitmaps/arrow_sm_right__3.png", + "ce/shell/bitmaps/arrow_sm_up__0.png", + "ce/shell/bitmaps/arrow_sm_up__1.png", + "ce/shell/bitmaps/black__0.png", + "ce/shell/bitmaps/blue__0.png", + "ce/shell/bitmaps/controller_border__0.png", + "ce/shell/bitmaps/device_border__0.png", + "ce/shell/bitmaps/game_type_grafix__0.png", + "ce/shell/bitmaps/game_type_grafix__1.png", + "ce/shell/bitmaps/game_type_grafix__2.png", + "ce/shell/bitmaps/game_type_grafix__3.png", + "ce/shell/bitmaps/game_type_grafix__4.png", + "ce/shell/bitmaps/game_type_grafix__5.png", + "ce/shell/bitmaps/gradient__0.png", + "ce/shell/bitmaps/gradient_big__0.png", + "ce/shell/bitmaps/join_game_border__0.png", + "ce/shell/bitmaps/list_field__0.png", + "ce/shell/bitmaps/list_item_bkd__0.png", + "ce/shell/bitmaps/list_item_bkd__1.png", + "ce/shell/bitmaps/list_item_bkd_top__0.png", + "ce/shell/bitmaps/list_item_bkd_top__1.png", + "ce/shell/bitmaps/locked_gametype__0.png", + "ce/shell/bitmaps/locked_gametype__1.png", + "ce/shell/bitmaps/option_bkds__0.png", + "ce/shell/bitmaps/option_bkds__1.png", + "ce/shell/bitmaps/option_bkds_sm2__0.png", + "ce/shell/bitmaps/option_bkds_sm2__1.png", + "ce/shell/bitmaps/option_bkds_sm__0.png", + "ce/shell/bitmaps/option_bkds_sm__1.png", + "ce/shell/bitmaps/sel_list_desc_bkd__0.png", + "ce/shell/bitmaps/sel_list_item_bkd__0.png", + "ce/shell/bitmaps/sel_list_item_bkd__1.png", + "ce/shell/bitmaps/sel_list_item_bkd__2.png", + "ce/shell/bitmaps/sel_list_item_bkd_top__0.png", + "ce/shell/bitmaps/sel_list_item_bkd_top__1.png", + "ce/shell/bitmaps/sel_list_item_bkd_top__2.png", + "ce/shell/bitmaps/semi_transparent_grey__0.png", + "ce/shell/bitmaps/server_header_background_0__0.png", + "ce/shell/bitmaps/server_header_background_0__1.png", + "ce/shell/bitmaps/server_header_background_1__0.png", + "ce/shell/bitmaps/server_header_background_1__1.png", + "ce/shell/bitmaps/server_header_background_2__0.png", + "ce/shell/bitmaps/server_header_background_2__1.png", + "ce/shell/bitmaps/server_header_background_3__0.png", + "ce/shell/bitmaps/server_header_background_3__1.png", + "ce/shell/bitmaps/server_item_bkds__0.png", + "ce/shell/bitmaps/server_item_bkds__1.png", + "ce/shell/bitmaps/server_item_bkds__2.png", + "ce/shell/bitmaps/spinner_list_3_wide_item_background__0.png", + "ce/shell/bitmaps/spinner_list_3_wide_item_background__1.png", + "ce/shell/bitmaps/text_button_background__0.png", + "ce/shell/bitmaps/text_button_background__1.png", + "ce/shell/bitmaps/white__0.png", + "ce/shell/main_menu/campaign_select/header_load_game__0.png", + "ce/shell/main_menu/campaign_select/item_back__0.png", + "ce/shell/main_menu/campaign_select/item_back__1.png", + "ce/shell/main_menu/campaign_select/item_continue__0.png", + "ce/shell/main_menu/campaign_select/item_continue__1.png", + "ce/shell/main_menu/campaign_select/item_load_game__0.png", + "ce/shell/main_menu/campaign_select/item_load_game__1.png", + "ce/shell/main_menu/campaign_select/item_new_campaign__0.png", + "ce/shell/main_menu/campaign_select/item_new_campaign__1.png", + "ce/shell/main_menu/difficulty_select/header_choose_difficulty__0.png", + "ce/shell/main_menu/gametype_select/header_select_gametype__0.png", + "ce/shell/main_menu/menu_load_campaign__0.png", + "ce/shell/main_menu/menu_load_campaign__1.png", + "ce/shell/main_menu/menu_multiplayer__0.png", + "ce/shell/main_menu/menu_multiplayer__1.png", + "ce/shell/main_menu/menu_profile__0.png", + "ce/shell/main_menu/menu_profile__1.png", + "ce/shell/main_menu/menu_quit__0.png", + "ce/shell/main_menu/menu_quit__1.png", + "ce/shell/main_menu/menu_settings__0.png", + "ce/shell/main_menu/menu_settings__1.png", + "ce/shell/main_menu/multiplayer_type_select/direct_ip/header_direct_ip__0.png", + "ce/shell/main_menu/multiplayer_type_select/header_multiplayer__0.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__0.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__1.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__2.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/classic_icon__3.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__0.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__1.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__2.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__3.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/filters/header_filters__0.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/header_internet__0.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/header_lan__0.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__0.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__1.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__2.png", + "ce/shell/main_menu/multiplayer_type_select/join_game/locked_icon__3.png", + "ce/shell/main_menu/multiplayer_type_select/mp_map_select/header_select_map__0.png", + "ce/shell/main_menu/multiplayer_type_select/mp_options__0.png", + "ce/shell/main_menu/multiplayer_type_select/mp_options__1.png", + "ce/shell/main_menu/multiplayer_type_select/server_settings/header_server_settings__0.png", + "ce/shell/main_menu/player_profiles_select/header_select_profile__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/indicator_options_edit/header_indicator_options__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/item_options_edit/header_item_options__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/name_edit/header_gametype_name__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/player_options_edit/header_player_options__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/header_rules__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__1.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__2.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__3.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__4.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__5.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__1.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__3.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__4.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__6.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__7.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_edit_multiplayer_gametype__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_set_options__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/playlist_select/header_select_gametype_to_edit__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/teamplay_options_edit/header_teamplay_options__0.png", + "ce/shell/main_menu/settings_select/multiplayer_setup/vehicle_options_edit/header_vehicle_options__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/audio_settings/header_profile_audio_settings__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/color_edit/header_profile_color__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/controller_edit/header_profile_controller_setting__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/controls_setup/header_profile_controls_settings__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/header_edit_profile_settings__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/header_profile_mouse_settings__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/name_edit/header_profile_name__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings__0.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__1.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__3.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__4.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__5.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png", + "ce/shell/main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings__0.png", + "ce/shell/main_menu/solo_level_select/header_load_level__0.png" + ] +} diff --git a/port/assets/menus/port_svg/main_menu/multiplayer_type_select/join_game/header_direct_link.svg b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/join_game/header_direct_link.svg new file mode 100644 index 00000000..84e31dfe --- /dev/null +++ b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/join_game/header_direct_link.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/menus/port_svg/main_menu/multiplayer_type_select/join_game/header_server_browser.svg b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/join_game/header_server_browser.svg new file mode 100644 index 00000000..455b3e4d --- /dev/null +++ b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/join_game/header_server_browser.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/menus/port_svg/main_menu/multiplayer_type_select/lobby/header_lobby.svg b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/lobby/header_lobby.svg new file mode 100644 index 00000000..c8e0330a --- /dev/null +++ b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/lobby/header_lobby.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/alert_bkd.svg b/port/assets/menus/svg/shell/bitmaps/alert_bkd.svg new file mode 100644 index 00000000..e95ddaa3 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/alert_bkd.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_down__0.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_down__0.svg new file mode 100644 index 00000000..91aab41a --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_down__0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_down__1.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_down__1.svg new file mode 100644 index 00000000..47b19525 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_down__1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__0.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__0.svg new file mode 100644 index 00000000..c3b1740d --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__1.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__1.svg new file mode 100644 index 00000000..e6c7bd32 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__2.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__2.svg new file mode 100644 index 00000000..afecae84 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__3.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__3.svg new file mode 100644 index 00000000..b5bacfb7 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_left__3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__0.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__0.svg new file mode 100644 index 00000000..1ed36b46 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__1.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__1.svg new file mode 100644 index 00000000..6845d795 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__2.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__2.svg new file mode 100644 index 00000000..dac57af1 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__3.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__3.svg new file mode 100644 index 00000000..1a0fd87b --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_right__3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_up__0.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_up__0.svg new file mode 100644 index 00000000..c1de1f72 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_up__0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/arrow_sm_up__1.svg b/port/assets/menus/svg/shell/bitmaps/arrow_sm_up__1.svg new file mode 100644 index 00000000..4b1aa244 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/arrow_sm_up__1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/black.svg b/port/assets/menus/svg/shell/bitmaps/black.svg new file mode 100644 index 00000000..b48afc81 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/black.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/blue.svg b/port/assets/menus/svg/shell/bitmaps/blue.svg new file mode 100644 index 00000000..87cd95cd --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/controller_border.svg b/port/assets/menus/svg/shell/bitmaps/controller_border.svg new file mode 100644 index 00000000..530a57be --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/controller_border.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/device_border.svg b/port/assets/menus/svg/shell/bitmaps/device_border.svg new file mode 100644 index 00000000..9669216f --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/device_border.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/game_type_grafix__0.svg b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__0.svg new file mode 100644 index 00000000..1aa32316 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__0.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/game_type_grafix__1.svg b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__1.svg new file mode 100644 index 00000000..7ead1f70 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__1.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/game_type_grafix__2.svg b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__2.svg new file mode 100644 index 00000000..d70bd103 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__2.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/game_type_grafix__3.svg b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__3.svg new file mode 100644 index 00000000..bf462cac --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__3.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/game_type_grafix__4.svg b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__4.svg new file mode 100644 index 00000000..df574518 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__4.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/game_type_grafix__5.svg b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__5.svg new file mode 100644 index 00000000..2e5822e3 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/game_type_grafix__5.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/gradient.svg b/port/assets/menus/svg/shell/bitmaps/gradient.svg new file mode 100644 index 00000000..da9b44cb --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/gradient.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/gradient_big.svg b/port/assets/menus/svg/shell/bitmaps/gradient_big.svg new file mode 100644 index 00000000..7bc511e4 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/gradient_big.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/join_game_border.svg b/port/assets/menus/svg/shell/bitmaps/join_game_border.svg new file mode 100644 index 00000000..ab11ef99 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/join_game_border.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/list_field.svg b/port/assets/menus/svg/shell/bitmaps/list_field.svg new file mode 100644 index 00000000..12ef05d1 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/list_field.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/list_item_bkd__0.svg b/port/assets/menus/svg/shell/bitmaps/list_item_bkd__0.svg new file mode 100644 index 00000000..34370b35 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/list_item_bkd__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/list_item_bkd__1.svg b/port/assets/menus/svg/shell/bitmaps/list_item_bkd__1.svg new file mode 100644 index 00000000..8b283a04 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/list_item_bkd__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/list_item_bkd_top__0.svg b/port/assets/menus/svg/shell/bitmaps/list_item_bkd_top__0.svg new file mode 100644 index 00000000..34370b35 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/list_item_bkd_top__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/list_item_bkd_top__1.svg b/port/assets/menus/svg/shell/bitmaps/list_item_bkd_top__1.svg new file mode 100644 index 00000000..c8814897 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/list_item_bkd_top__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/locked_gametype__0.svg b/port/assets/menus/svg/shell/bitmaps/locked_gametype__0.svg new file mode 100644 index 00000000..7d673adc --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/locked_gametype__0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/locked_gametype__1.svg b/port/assets/menus/svg/shell/bitmaps/locked_gametype__1.svg new file mode 100644 index 00000000..c0a982b5 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/locked_gametype__1.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/option_bkds__0.svg b/port/assets/menus/svg/shell/bitmaps/option_bkds__0.svg new file mode 100644 index 00000000..5302ed6e --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/option_bkds__0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/option_bkds__1.svg b/port/assets/menus/svg/shell/bitmaps/option_bkds__1.svg new file mode 100644 index 00000000..2ca33377 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/option_bkds__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/option_bkds_sm2__0.svg b/port/assets/menus/svg/shell/bitmaps/option_bkds_sm2__0.svg new file mode 100644 index 00000000..9352d3b2 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/option_bkds_sm2__0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/option_bkds_sm2__1.svg b/port/assets/menus/svg/shell/bitmaps/option_bkds_sm2__1.svg new file mode 100644 index 00000000..1caf2ed0 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/option_bkds_sm2__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/option_bkds_sm__0.svg b/port/assets/menus/svg/shell/bitmaps/option_bkds_sm__0.svg new file mode 100644 index 00000000..6125c096 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/option_bkds_sm__0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/option_bkds_sm__1.svg b/port/assets/menus/svg/shell/bitmaps/option_bkds_sm__1.svg new file mode 100644 index 00000000..b28bce14 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/option_bkds_sm__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/sel_list_desc_bkd.svg b/port/assets/menus/svg/shell/bitmaps/sel_list_desc_bkd.svg new file mode 100644 index 00000000..4116546d --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/sel_list_desc_bkd.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__0.svg b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__0.svg new file mode 100644 index 00000000..34370b35 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__1.svg b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__1.svg new file mode 100644 index 00000000..a0f4ac19 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__2.svg b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__2.svg new file mode 100644 index 00000000..3329f2f2 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd__2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__0.svg b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__0.svg new file mode 100644 index 00000000..34370b35 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__1.svg b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__1.svg new file mode 100644 index 00000000..6572e59f --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__2.svg b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__2.svg new file mode 100644 index 00000000..3329f2f2 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/sel_list_item_bkd_top__2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/semi_transparent_grey.svg b/port/assets/menus/svg/shell/bitmaps/semi_transparent_grey.svg new file mode 100644 index 00000000..fec03fe9 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/semi_transparent_grey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_header_background_0__0.svg b/port/assets/menus/svg/shell/bitmaps/server_header_background_0__0.svg new file mode 100644 index 00000000..1f1d899d --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_header_background_0__0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_header_background_0__1.svg b/port/assets/menus/svg/shell/bitmaps/server_header_background_0__1.svg new file mode 100644 index 00000000..b6b808b4 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_header_background_0__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_header_background_1__0.svg b/port/assets/menus/svg/shell/bitmaps/server_header_background_1__0.svg new file mode 100644 index 00000000..42872535 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_header_background_1__0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_header_background_1__1.svg b/port/assets/menus/svg/shell/bitmaps/server_header_background_1__1.svg new file mode 100644 index 00000000..6ac19a5b --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_header_background_1__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_header_background_2__0.svg b/port/assets/menus/svg/shell/bitmaps/server_header_background_2__0.svg new file mode 100644 index 00000000..b3961ff2 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_header_background_2__0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_header_background_2__1.svg b/port/assets/menus/svg/shell/bitmaps/server_header_background_2__1.svg new file mode 100644 index 00000000..0afb82ed --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_header_background_2__1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_header_background_3__0.svg b/port/assets/menus/svg/shell/bitmaps/server_header_background_3__0.svg new file mode 100644 index 00000000..10072f3d --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_header_background_3__0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_header_background_3__1.svg b/port/assets/menus/svg/shell/bitmaps/server_header_background_3__1.svg new file mode 100644 index 00000000..1d7b0889 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_header_background_3__1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_item_bkds__0.svg b/port/assets/menus/svg/shell/bitmaps/server_item_bkds__0.svg new file mode 100644 index 00000000..aa7fbdd2 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_item_bkds__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_item_bkds__1.svg b/port/assets/menus/svg/shell/bitmaps/server_item_bkds__1.svg new file mode 100644 index 00000000..ad5512f0 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_item_bkds__1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/server_item_bkds__2.svg b/port/assets/menus/svg/shell/bitmaps/server_item_bkds__2.svg new file mode 100644 index 00000000..85b017d7 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/server_item_bkds__2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/spinner_list_3_wide_item_background__0.svg b/port/assets/menus/svg/shell/bitmaps/spinner_list_3_wide_item_background__0.svg new file mode 100644 index 00000000..70f4c33c --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/spinner_list_3_wide_item_background__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/bitmaps/spinner_list_3_wide_item_background__1.svg b/port/assets/menus/svg/shell/bitmaps/spinner_list_3_wide_item_background__1.svg new file mode 100644 index 00000000..ea713313 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/spinner_list_3_wide_item_background__1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/text_button_background__0.svg b/port/assets/menus/svg/shell/bitmaps/text_button_background__0.svg new file mode 100644 index 00000000..93605439 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/text_button_background__0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/text_button_background__1.svg b/port/assets/menus/svg/shell/bitmaps/text_button_background__1.svg new file mode 100644 index 00000000..0d450437 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/text_button_background__1.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/bitmaps/white.svg b/port/assets/menus/svg/shell/bitmaps/white.svg new file mode 100644 index 00000000..3a32cd89 --- /dev/null +++ b/port/assets/menus/svg/shell/bitmaps/white.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/header_load_game.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/header_load_game.svg new file mode 100644 index 00000000..8526d938 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/header_load_game.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/item_back__0.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/item_back__0.svg new file mode 100644 index 00000000..7242c431 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/item_back__0.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/item_back__1.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/item_back__1.svg new file mode 100644 index 00000000..7f12793e --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/item_back__1.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/item_continue__0.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/item_continue__0.svg new file mode 100644 index 00000000..7ec50949 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/item_continue__0.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/item_continue__1.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/item_continue__1.svg new file mode 100644 index 00000000..fe8c39bb --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/item_continue__1.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/item_load_game__0.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/item_load_game__0.svg new file mode 100644 index 00000000..9119d39d --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/item_load_game__0.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/item_load_game__1.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/item_load_game__1.svg new file mode 100644 index 00000000..5c94bec1 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/item_load_game__1.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/item_new_campaign__0.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/item_new_campaign__0.svg new file mode 100644 index 00000000..d5a32148 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/item_new_campaign__0.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/campaign_select/item_new_campaign__1.svg b/port/assets/menus/svg/shell/main_menu/campaign_select/item_new_campaign__1.svg new file mode 100644 index 00000000..526cca48 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/campaign_select/item_new_campaign__1.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/difficulty_select/header_choose_difficulty.svg b/port/assets/menus/svg/shell/main_menu/difficulty_select/header_choose_difficulty.svg new file mode 100644 index 00000000..8aa32c54 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/difficulty_select/header_choose_difficulty.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/gametype_select/header_select_gametype.svg b/port/assets/menus/svg/shell/main_menu/gametype_select/header_select_gametype.svg new file mode 100644 index 00000000..99321eb0 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/gametype_select/header_select_gametype.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_load_campaign__0.svg b/port/assets/menus/svg/shell/main_menu/menu_load_campaign__0.svg new file mode 100644 index 00000000..bdddd898 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_load_campaign__0.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_load_campaign__1.svg b/port/assets/menus/svg/shell/main_menu/menu_load_campaign__1.svg new file mode 100644 index 00000000..61105676 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_load_campaign__1.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_multiplayer__0.svg b/port/assets/menus/svg/shell/main_menu/menu_multiplayer__0.svg new file mode 100644 index 00000000..b429c083 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_multiplayer__0.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_multiplayer__1.svg b/port/assets/menus/svg/shell/main_menu/menu_multiplayer__1.svg new file mode 100644 index 00000000..7869a337 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_multiplayer__1.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_profile__0.svg b/port/assets/menus/svg/shell/main_menu/menu_profile__0.svg new file mode 100644 index 00000000..c04194de --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_profile__0.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_profile__1.svg b/port/assets/menus/svg/shell/main_menu/menu_profile__1.svg new file mode 100644 index 00000000..25267e7d --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_profile__1.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_quit__0.svg b/port/assets/menus/svg/shell/main_menu/menu_quit__0.svg new file mode 100644 index 00000000..8fbf2cf1 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_quit__0.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_quit__1.svg b/port/assets/menus/svg/shell/main_menu/menu_quit__1.svg new file mode 100644 index 00000000..54460eba --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_quit__1.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_settings__0.svg b/port/assets/menus/svg/shell/main_menu/menu_settings__0.svg new file mode 100644 index 00000000..36bd702b --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_settings__0.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/menu_settings__1.svg b/port/assets/menus/svg/shell/main_menu/menu_settings__1.svg new file mode 100644 index 00000000..93ee7163 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/menu_settings__1.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/direct_ip/header_direct_ip.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/direct_ip/header_direct_ip.svg new file mode 100644 index 00000000..8931ce5f --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/direct_ip/header_direct_ip.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/header_multiplayer.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/header_multiplayer.svg new file mode 100644 index 00000000..bb75c453 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/header_multiplayer.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__0.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__0.svg new file mode 100644 index 00000000..4cf88389 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__1.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__1.svg new file mode 100644 index 00000000..841f2a57 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__1.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__2.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__2.svg new file mode 100644 index 00000000..4f1528c2 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__2.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__3.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__3.svg new file mode 100644 index 00000000..78c15723 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/classic_icon__3.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__0.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__0.svg new file mode 100644 index 00000000..4cf88389 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__1.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__1.svg new file mode 100644 index 00000000..4d863ecf --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__1.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__2.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__2.svg new file mode 100644 index 00000000..b26c2c3f --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__3.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__3.svg new file mode 100644 index 00000000..d338dd2f --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/dedicated_icon__3.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/filters/header_filters.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/filters/header_filters.svg new file mode 100644 index 00000000..869343e6 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/filters/header_filters.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/header_internet.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/header_internet.svg new file mode 100644 index 00000000..502b2c54 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/header_internet.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/header_lan.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/header_lan.svg new file mode 100644 index 00000000..f92c9d9c --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/header_lan.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__0.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__0.svg new file mode 100644 index 00000000..4cf88389 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__0.svg @@ -0,0 +1,3 @@ + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__1.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__1.svg new file mode 100644 index 00000000..f586f2a6 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__1.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__2.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__2.svg new file mode 100644 index 00000000..c75974cf --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__3.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__3.svg new file mode 100644 index 00000000..ac048b5a --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/join_game/locked_icon__3.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/mp_map_select/header_select_map.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/mp_map_select/header_select_map.svg new file mode 100644 index 00000000..d9934d9c --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/mp_map_select/header_select_map.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/server_settings/header_server_settings.svg b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/server_settings/header_server_settings.svg new file mode 100644 index 00000000..25edfa53 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/multiplayer_type_select/server_settings/header_server_settings.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/player_profiles_select/header_select_profile.svg b/port/assets/menus/svg/shell/main_menu/player_profiles_select/header_select_profile.svg new file mode 100644 index 00000000..212ab5f8 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/player_profiles_select/header_select_profile.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/indicator_options_edit/header_indicator_options.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/indicator_options_edit/header_indicator_options.svg new file mode 100644 index 00000000..3cb4a396 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/indicator_options_edit/header_indicator_options.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/item_options_edit/header_item_options.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/item_options_edit/header_item_options.svg new file mode 100644 index 00000000..9ef0d734 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/item_options_edit/header_item_options.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/name_edit/header_gametype_name.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/name_edit/header_gametype_name.svg new file mode 100644 index 00000000..f3cc6058 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/name_edit/header_gametype_name.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/player_options_edit/header_player_options.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/player_options_edit/header_player_options.svg new file mode 100644 index 00000000..ae525858 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/player_options_edit/header_player_options.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/header_rules.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/header_rules.svg new file mode 100644 index 00000000..0228ba75 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/header_rules.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__0.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__0.svg new file mode 100644 index 00000000..e34d52b6 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__0.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__1.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__1.svg new file mode 100644 index 00000000..408eab4b --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__1.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__2.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__2.svg new file mode 100644 index 00000000..af94e51b --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__3.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__3.svg new file mode 100644 index 00000000..671ef7fa --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__3.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__4.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__4.svg new file mode 100644 index 00000000..9a8a3b5f --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__4.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__5.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__5.svg new file mode 100644 index 00000000..f6374e10 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/rules_options__5.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__1.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__1.svg new file mode 100644 index 00000000..f820daf7 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_edit_multiplayer_gametype.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_edit_multiplayer_gametype.svg new file mode 100644 index 00000000..1cb8862c --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_edit_multiplayer_gametype.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_set_options.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_set_options.svg new file mode 100644 index 00000000..3127aa67 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_edit/header_set_options.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_select/header_select_gametype_to_edit.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_select/header_select_gametype_to_edit.svg new file mode 100644 index 00000000..2b01f235 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/playlist_select/header_select_gametype_to_edit.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/teamplay_options_edit/header_teamplay_options.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/teamplay_options_edit/header_teamplay_options.svg new file mode 100644 index 00000000..4e431c70 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/teamplay_options_edit/header_teamplay_options.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/vehicle_options_edit/header_vehicle_options.svg b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/vehicle_options_edit/header_vehicle_options.svg new file mode 100644 index 00000000..810331c5 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/multiplayer_setup/vehicle_options_edit/header_vehicle_options.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/audio_settings/header_profile_audio_settings.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/audio_settings/header_profile_audio_settings.svg new file mode 100644 index 00000000..88a2cbd7 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/audio_settings/header_profile_audio_settings.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/color_edit/header_profile_color.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/color_edit/header_profile_color.svg new file mode 100644 index 00000000..7a828fb0 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/color_edit/header_profile_color.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/controller_edit/header_profile_controller_setting.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/controller_edit/header_profile_controller_setting.svg new file mode 100644 index 00000000..697b8c27 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/controller_edit/header_profile_controller_setting.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/controls_setup/header_profile_controls_settings.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/controls_setup/header_profile_controls_settings.svg new file mode 100644 index 00000000..a933918c --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/controls_setup/header_profile_controls_settings.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/header_edit_profile_settings.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/header_edit_profile_settings.svg new file mode 100644 index 00000000..db3271db --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/header_edit_profile_settings.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/header_profile_mouse_settings.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/header_profile_mouse_settings.svg new file mode 100644 index 00000000..8cdd1b2d --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/header_profile_mouse_settings.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/name_edit/header_profile_name.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/name_edit/header_profile_name.svg new file mode 100644 index 00000000..e41b8e17 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/name_edit/header_profile_name.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings.svg new file mode 100644 index 00000000..68c3ed34 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings.svg b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings.svg new file mode 100644 index 00000000..a8212101 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/svg/shell/main_menu/solo_level_select/header_load_level.svg b/port/assets/menus/svg/shell/main_menu/solo_level_select/header_load_level.svg new file mode 100644 index 00000000..0f3979c8 --- /dev/null +++ b/port/assets/menus/svg/shell/main_menu/solo_level_select/header_load_level.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/linux/README.md b/port/linux/README.md index 5cd7123e..3114ca94 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -91,30 +91,33 @@ backtrace to the standard error. To find the function at the address, enter ## Controls -The keyboard and the mouse operate controller 1. The game adds the input of -the first gamepad to controller 1. The other gamepads operate controllers 2 -to 4. +The keyboard and the mouse are a control scheme of their own for the player +of controller 1: each action has up to two keys or mouse buttons, which +Settings > Controls Setup (or `[controls]` in `config.toml`) changes. The +game adds the input of the first gamepad to controller 1. The other +gamepads operate controllers 2 to 4. The profile's button layout (Settings > +Gamepads) is the gamepads' only. -| Key | Controller | Function in the game | -| --- | --- | --- | -| W, A, S, D | left stick | move | -| mouse | (direct aim) | aim | -| left mouse button | right trigger | fire | -| right mouse button, G | left trigger | throw a grenade | -| space, enter | A | jump, accept | -| F, backspace, mouse button 4 | B | melee, back | -| E, R | X | action, reload | -| tab, mouse wheel | Y | change the weapon | -| Q | white | flashlight | -| X | black | change the grenade | -| left ctrl, C | left stick click | crouch | -| Z, middle mouse button | right stick click | zoom | -| arrow keys | D-pad | | -| escape | start | pause menu | -| F1 | back | | -| \` | | open the developer console | -| F12 | | release or capture the mouse | -| F11 | | change between fullscreen and window | +| Action | Keys and buttons (default) | +| --- | --- | +| move | W, A, S, D | +| aim | mouse (direct aim) | +| fire | left mouse button | +| throw a grenade | right mouse button, G | +| jump (and skip a cutscene) | space | +| crouch | left ctrl, C | +| melee | F, mouse button 4 | +| reload | R | +| action (pick up, hold to swap weapons, enter or leave a vehicle; never reloads) | E | +| change the weapon | mouse wheel, 1 | +| change the grenade | X | +| flashlight | Q | +| zoom | Z, middle mouse button | +| show the scores (hold) | tab | +| pause menu | escape | + +Always: \` opens the developer console, F12 releases or captures the mouse, +F11 changes between fullscreen and window. One movement of the mouse wheel changes the weapon one time. A second movement after a short pause changes it again. @@ -128,10 +131,40 @@ In the menus, the mouse moves a pointer: - A right click goes back. - The mouse wheel moves through the items. -The keyboard also operates the menus. When the game continues, the mouse -aims again. A mouse button that you hold from the menu does not fire until +The keyboard also operates the menus, with keys of its own: the arrow keys +(and W, A, S, D) move, space or enter selects, escape or backspace goes +back (escape resumes the game from the pause menu), delete deletes. When the game continues, the mouse aims again. A mouse button that you hold from the menu does not fire until you push it again. +## Menus + +The menus are the PC version's (Halo Custom Edition's): its main menu and +every screen it leads to (campaign, profiles, multiplayer with its server +browser, direct IP and server setup, the gametype editor, and the profile's +settings: controls, gamepads, mouse, audio, video, network and colour), +laid out as it has them. Their pictures are the high-res redraws; the few +that are not yet (3D renders, screenshots) are drawn from the Xbox's own +menus where it has the same, else a placeholder, listed in +`port/assets/menus/NON_HANDDRAWN.md`. + +The campaign is wired: Campaign, on player 1's profile (the one last used), +continues its saved game, starts a level it has reached at a difficulty (New +Game), or loads or deletes any profile's saved game (Load Game). Many of the +other functions behind the screens are not wired to this game yet +(`port/assets/menus/UNWIRED.md`): the lists they fill (profiles, maps, +servers, key bindings) are empty, and the settings they change do not +change. The screens open, close and move between each other as the PC +version's. `display.menus = "xbox"` gives the Xbox's menus, which start every +kind of game. + +The menus are XML files in `port/assets/menus` (`tools/ce_menus.py` writes +them from the PC version's tags), which the game contains. To change them, +put files in a `menus` folder next to `config.toml`: a file with the same +path replaces one of the game's, and another `.xml` file is added. +`port/assets/menus/README.md` describes the files. If a file has a problem, +the game uses the Xbox's menus and the log names the file, the line and the +problem. + ## Settings The settings are in `config.toml` next to the executable @@ -139,16 +172,20 @@ The settings are in `config.toml` next to the executable with the default values and a comment for each setting. To get the default values again, delete the file. -The game reads the file one time, at start-up. If a key is not correct, or -a value has the wrong type, the game writes the line to the log and uses the -default value. +The game reads the file at start-up. If a key is not correct, or a value +has the wrong type, the game writes the line to the log and uses the default +value. The Settings menu (Video, Mouse, Audio, Network and Controls Setup) +changes the useful settings, writes them into the file (only their lines +change) and applies them at once, but `audio.enabled`, and `display.menus` +from the next main menu. Each setting has an environment variable. The environment variable changes the setting for one start of the game. It has priority over the file. | Setting | Default | Environment variable | Function | | --- | --- | --- | --- | -| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: fullscreen at the resolution of the display. The picture has 480 lines of the game and the width of the display. `false`: a window with the 640x480 picture of the Xbox. F11 changes between the two. | +| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at its desktop resolution. `"borderless"`: a window over the whole desktop. Both draw at the resolution of the display, the picture 480 lines of the game and the width of the display. `"windowed"`: a window with the 640x480 picture of the Xbox, scaled. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. | +| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: fullscreen at the resolution of the display. The picture has 480 lines of the game and the width of the display. `false`: a window with the 640x480 picture of the Xbox. Used when `display.mode` is empty. | | `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | The size of the window, as a multiple of 640x480. You can change the size of the window. | | `display.vsync` | `true` | `HALO_NO_VSYNC=1` sets `false` | `true`: each frame waits for the display. | | `display.max_fps` | `0` | `HALO_MAX_FPS` | With vsync off, the most frames each second. `0`: twice the display's refresh rate. `-1`: no limit, which can hang some Intel graphics (Raptor Lake), resetting the desktop's graphics too. | @@ -157,16 +194,21 @@ the setting for one start of the game. It has priority over the file. | `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". | | `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. | | `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the display's resolution, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. | +| `display.menus` | `"pc"` | `HALO_MENUS` | `"pc"`: the PC version's menus, from the files in `port/assets/menus` and a `menus` folder next to `config.toml`. Refer to "Menus". `"xbox"`: the Xbox's menus. | | `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only while the enemy is in sight and not camouflaged, so it never shows where an enemy hides, and only as far away as the weapon in hand turns its reticle red over an enemy (at least 20 world units, the motion sensor's reach, and at most 70). | | `display.player_name_scale` | `1.0` | `HALO_PLAYER_NAME_SCALE` | How large the players' names are drawn: `1.0` is three quarters of the size of the HUD's text, from `0.25` to `4`. With high-res text, larger names are rasterized at their size, so they stay sharp. | -| `display.scoreboard_team_layout` | `"teams"` | `HALO_SCOREBOARD_TEAM_LAYOUT` | How the multiplayer scoreboard (hold BACK, or F1) lists a team game's players. `"teams"`: a column for each team, red on the left and blue on the right. `"score"`: all the players in order of score. With more players than fit, the mouse wheel and Page Up / Page Down scroll the scoreboard. | -| `display.scoreboard_background` | `true` | `HALO_SCOREBOARD_BACKGROUND` | `true`: the multiplayer scoreboard (hold BACK, or F1) has a panel behind its text, for clearer text. | +| `display.scoreboard_team_layout` | `"teams"` | `HALO_SCOREBOARD_TEAM_LAYOUT` | How the multiplayer scoreboard (hold BACK, or tab) lists a team game's players. `"teams"`: a column for each team, red on the left and blue on the right. `"score"`: all the players in order of score. With more players than fit, the mouse wheel and Page Up / Page Down scroll the scoreboard. | +| `display.scoreboard_background` | `true` | `HALO_SCOREBOARD_BACKGROUND` | `true`: the multiplayer scoreboard (hold BACK, or tab) has a panel behind its text, for clearer text. | | `display.scoreboard_background_color` | `"16, 16, 16, 150"` | `HALO_SCOREBOARD_BACKGROUND_COLOR` | The colour of the scoreboard's panel: `"red, green, blue, alpha"`, each from `0` to `255`. Alpha `0` is see-through, `255` is solid. | | `audio.enabled` | `true` | `HALO_NO_AUDIO=1` sets `false` | `false`: no audio device. The sound continues without output. | | `audio.volume` | `1.0` | `HALO_VOLUME` | The master volume. | +| `audio.music_volume` | `1.0` | `HALO_MUSIC_VOLUME` | The music's volume, of the master volume. | +| `audio.effects_volume` | `1.0` | `HALO_EFFECTS_VOLUME` | The volume of the other sounds (effects and speech), of the master volume. | | `input.mouse_sensitivity` | `1.0` | `HALO_MOUSE_SENSITIVITY` | The multiplier for the mouse aim. | +| `input.mouse_vertical_sensitivity` | `0.0` | `HALO_MOUSE_VERTICAL_SENSITIVITY` | The multiplier for the vertical mouse aim. `0`: the same as `input.mouse_sensitivity`. | | `input.invert_mouse` | `false` | `HALO_MOUSE_INVERT=1` sets `true` | `true`: the vertical mouse aim is inverted. | | `input.mouse_aim_assist` | `false` | `HALO_MOUSE_AIM_ASSIST` | `true`: the magnetism of the controller also operates for the mouse. `false`: when the mouse moved after the right stick, the view is not slowed or dragged by a target. The autoaim of the bullets operates in both cases. | +| `controls.` | (the table in "Controls") | `HALO_KEY_` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. | | `game.console_log` | `"important"` | `HALO_CONSOLE_LOG` | What the console shows on the screen. `"important"`: bans, players that the host drops for cheating, the reasons that the game refuses a command, and the asserts that stop the game. `"all"`: all the lines. `"none"`: only the asserts that stop the game. The output of a command always shows. `debug.txt` gets all the lines. | | `game.language` | `""` | `HALO_LANGUAGE` | The language of the menus: `ja`, `de`, `fr`, `es` or `it`. Empty: English. | | `paths.data` | `""` | `HALO_DATA_ROOT` | The data root. Refer to "Start the game". | @@ -186,6 +228,7 @@ the setting for one start of the game. It has priority over the file. | `debug.screenshot_directory`, `debug.screenshot_every` | `""`, `0` | `HALO_SCREENSHOT_DIR`, `HALO_SCREENSHOT_EVERY` | The game writes each Nth frame to this folder as a BMP file. | | `debug.hidden_window`, `debug.null_renderer` | `false` | `HALO_HIDDEN_WINDOW`, `HALO_NULL_RENDERER` | `true`: no visible window, or no graphics. | | `debug.gpu_stats`, `debug.gpu_trace_frame`, `debug.gpu_trace_constants`, `debug.gpu_dump_shaders`, `debug.texture_dump_directory`, `debug.texture_log`, `debug.gl_debug`, `debug.texture_no_cache` | off | `HALO_GPU_STATS`, `HALO_GPU_TRACE`, `HALO_GPU_TRACE_CONSTANTS`, `HALO_GPU_DUMP_SHADERS`, `HALO_TEXTURE_DUMP`, `HALO_TEXTURE_LOG`, `HALO_GL_DEBUG`, `HALO_TEXTURE_NO_CACHE` | Tools to find problems in the graphics: counts for each frame, all the GL state of one frame, the GLSL code, the textures. | +| `debug.menu_open` | `""` | `HALO_MENU_OPEN` | Start on this screen of the menus (`main_menu/settings_select/...`, as `port/assets/menus` names it), a player profile being edited, to look at it. | | `debug.gpu_skip_vertex_shaders`, `debug.gpu_debug_expression`, `debug.gpu_debug_flat`, `debug.gpu_debug_texture0` | off | `HALO_GPU_SKIP_VS`, `HALO_GPU_DEBUG_EXPR`, `HALO_GPU_DEBUG_FLAT`, `HALO_GPU_DEBUG_T0` | Tools to find problems in the graphics: skip the draws of a vertex shader, or replace the output of all pixel shaders with a GLSL expression (for example `t0.rgb`). | | `debug.network_test`, `debug.network_test_start`, `debug.network_test_kill`, `debug.network_test_score`, `debug.network_test_shoot`, `debug.network_test_vehicle`, `debug.network_test_pickup`, `debug.network_test_pickup_weapon`, `debug.test_input` | off | `HALO_NETWORK_TEST`, `HALO_NETWORK_TEST_START`, `HALO_NETWORK_TEST_KILL`, `HALO_NETWORK_TEST_SCORE`, `HALO_NETWORK_TEST_SHOOT`, `HALO_NETWORK_TEST_VEHICLE`, `HALO_NETWORK_TEST_PICKUP`, `HALO_NETWORK_TEST_PICKUP_WEAPON`, `HALO_TEST_INPUT` | Automatic tests of system link (`game/network_test.c`). Refer to `NETCODE.md`. | | `debug.network_latency`, `debug.network_loss` | `0` | `HALO_NETWORK_LATENCY`, `HALO_NETWORK_LOSS` | The game holds all the data that it receives for this number of milliseconds, and ignores this percentage of the datagrams. Use these settings to test the netcode as on the internet. | @@ -564,7 +607,10 @@ Other changes: | `cseries/errors.c` | `debug.txt` stays open between lines. | | `networking/`, `game/`, `interface/`, `bungie_net/network/` and the pools of objects, effects and sounds | The system link limits and the memory for them. | | `game/`, `objects/`, `units/`, `networking/` | The distributed netcode. Refer to `NETCODE.md`. | -| `cache/cache_files.c` | When a map's tags load and unload, the port finds the bitmaps that the high-res HUD replaces (`game/hud_hires_tags.c`). | +| `cache/cache_files.c` | When a map's tags load and unload, the port finds the bitmaps that the high-res HUD replaces (`game/hud_hires_tags.c`), and adds the tags of the menus to the menus' map (`game/menu_tags.c`). | +| `interface/ui_widget.c`, `interface/ui_widget_event_handler_functions.c`, `interface/ui_widget_game_data_input_functions.c` | The main menu is the PC version's from `port/assets/menus` (`display.menus`); the menus' widgets can call the port's functions (`game/menu_functions.c`) and send the PC version's custom activation event; the widgets' memory is 256 KB, not 16 KB. | +| `input/input_abstraction.c`, `game/player_control.c`, `game/players.c`, `game/player_queues_new.c`, `units/units.h` | The keyboard and mouse's actions (`src/xinput_sdl.c`, `include/halo_keyboard.h`) join controller 1's game controls; their reload key reloads on its own, and their action key only acts (a control flag of the port's, sent with the player's action, stops the reload the controller's X falls back to). | +| `sound/sound_manager.c`, `interface/hud.c`, `game/game_engine.c` | The music's and the other sounds' volumes; the HUD's and the scoreboard's settings are read again when Settings changes them. | | `interface/hud.c` | In multiplayer, players' names are drawn above their heads (`display.player_names`, `display.player_name_scale`). | | `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the display's resolution (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. | diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c new file mode 100644 index 00000000..fd695d52 --- /dev/null +++ b/port/linux/game/menu_functions.c @@ -0,0 +1,3668 @@ +/* +MENU_FUNCTIONS.C + +The event handler functions the menus written in XML can run besides the +game's (menu_tags.c names them; ui_widget_event_handler_function_invoke +calls them from PC_MENU_FUNCTION_BASE on): + +- "port quit game" (and the PC version's "main menu quit game") quits, as + closing the window does; +- the PC version's "profile set edit begin" begins editing the first player + profile, as its settings screens need, and fails with none (its handlers + then open the screens that make one); +- "gamespy screen init" hides the server browser's error and filter panels, + and the title of the mode "mp type set mode" did not choose (Internet or + LAN); +- its buttons' events: "mouse emit back event" and "mouse emit x event" push + B and X (as the mouse does), "emit custom activation event" (an OK + button) runs the screen's custom activation handler, "single prev cl item + activated" (an item chosen) its list's, and its back handlers go back; +- "port setting load", on a spinner's created event, shows the value its + setting has (or the nearest of its values): config.toml's, or for + "profile." the controller's in the profile being edited; +- "port setting save", on its deleted event, writes the value shown back, + if the player changed it; +- the settings screens' (tools/port_settings.py): "port settings save" + (OK) writes those changed and applies the window's (the rest follow + config.toml themselves), "port settings defaults" shows the defaults, + "port settings help" the help of the row chosen; +- Controls Setup's: the keyboard and mouse's controls, two bindings each, + shown a group at a time ("controls update menu"); "controls begin + binding" takes the next key or button pressed for the binding left and + right choose ("controls binding slot"); OK ("controls screen change set") + writes them, "controls screen defaults" shows the defaults. + +A spinner's values are its strings' (setting= and values= in its , +menu_tags.c's pc_menu_setting). + +The PC version's campaign menus, on player 1's profile (the active one, else +the one last used, else the first) and the game's saved game in it: +- "campaign menu continue" goes on with the saved game; +- New Game's list of levels ("initialize sp level list solo", "solo map + list update", "solo level set map") has those the profile has reached, and + marks those it has finished on Normal, Heroic and Legendary, as the + Xbox's list does; a level with the saved game in it goes on with it, at + its difficulty; +- the difficulty menu: "difficulty item select" (a difficulty chosen) and + "set difficulty" (its OK button: the difficulty shown) start the game; +- Load Game's list ("load game menu init", "load game list update", "load + game menu activated") has each profile's saved game, and "load game menu + delete request" and "delete finish" delete one. +The Xbox's functions of those names take the Xbox's widgets (a spinner of +levels, the difficulty list itself), so ours run instead (menu_tags.c). + +The rest are the PC version's, which its menus (port/assets/menus/ce) +name and the Xbox's has not: they do nothing yet, and succeed, so that what +their handlers open opens. +*/ + +#include "cseries.h" +#include "input/input.h" +#include "interface/event_manager.h" +#include "interface/player_ui.h" +#include "interface/ui_widget.h" +#include "main/main.h" +#include "networking/network_game_manager.h" +#include "saved games/player_profile.h" +#include "tag_files/tag_groups.h" +#include "text/text_group.h" +#include "text/unicode.h" + +#include "halo_menus.h" + +#include +#include +#include + +/* the platform layer's (port/linux/src) */ +void platform_log(char const *format, ...); +void platform_request_quit(void); +char const *pc_menu_function_name(long function_index); +char const *pc_menu_game_data_input_name(long function_index); +void event_manager_post_button(short controller_index, short button_index); +int config_text(char const *name, char *text, unsigned int size); +int config_write(char const *name, char const *value); +int config_default(char const *name, char *text, unsigned int size); +char const *config_string(char const *name); +void platform_display_apply(void); +void platform_binding_capture_begin(void); +int platform_binding_capture_poll(int *input); +void halo_input_name(int input, char *name, unsigned int size); +short pc_menu_string_index(long definition_index); + +/* the game's (port) */ +boolean ui_widget_port_dispatch_event(struct widget_instance *widget, short event_type, short controller_index, + boolean *deleted); +void ui_widget_port_go_back(struct widget_instance *widget); +short ui_widget_port_list_index(struct widget_instance *list_widget); +boolean ui_widget_port_saved_game(char const **map_name, short *level, short *difficulty); +short main_get_solo_level_from_name(char const *name); + +boolean pc_menu_event_function_invoke(struct widget_instance *widget, struct event_record *event, + long function_index, boolean *widget_deleted); +void pc_menu_game_data_function_invoke(struct widget_instance *widget, long function); + +/* ---------- constants */ + +#define MAXIMUM_STRINGS 64 +/* the PC version's custom activation event (this engine never sends it) */ +#define EVENT_CUSTOM_ACTIVATION 32 +#define BUTTON_A 0 +#define BUTTON_B 1 +#define BUTTON_X 2 + +enum +{ + _pc_menu_function_quit_game, + _pc_menu_function_setting_load, + _pc_menu_function_setting_save, + NUMBER_OF_PC_MENU_FUNCTIONS +}; + +/* ---------- structures */ + +/* a widget, as ui_widget.c has it */ +struct widget_instance +{ + long definition_tag_index; + char const *name; + short local_player_index; + short horizontal_offset; + short vertical_offset; + short type; + boolean visible; + boolean render_regardless_of_controller_index; + boolean disabled; + boolean pause_game_time; + boolean delete_recursion_lock; + boolean widget_is_error_dialog; + boolean close_if_local_player_controller_present; + byte pad17; + long creation_time; + unsigned long milliseconds_to_auto_close; + unsigned long auto_close_fade_time; + real alpha_modifier; + struct widget_instance *previous; + struct widget_instance *next; + struct widget_instance *parent; + struct widget_instance *child; + struct widget_instance *focused_child; + union + { + struct + { + wchar_t *text; + short string_list_index; + } text_box; + struct + { + short selected_index; + short last_list_tab_direction; + void *list_items; + word number_of_items; + struct widget_instance *extended_description; + wchar_t *item_text; + } list; + } parameters; + struct + { + short current_frame_index; + short first_frame_index; + short last_frame_index; + short number_of_sprite_frames; + } animation; +}; + +typedef char verify_widget_instance_size[ + sizeof(struct widget_instance) == 0x58 ? 1 : -1]; +typedef char verify_widget_instance_child_offset[ + offsetof(struct widget_instance, child) == 0x34 ? 1 : -1]; +typedef char verify_widget_instance_parent_offset[ + offsetof(struct widget_instance, parent) == 0x30 ? 1 : -1]; +typedef char verify_widget_instance_selected_index_offset[ + offsetof(struct widget_instance, parameters.list.selected_index) == 0x3C ? 1 : -1]; +typedef char verify_widget_instance_number_of_items_offset[ + offsetof(struct widget_instance, parameters.list.number_of_items) == 0x44 ? 1 : -1]; +typedef char verify_widget_instance_animation_offset[ + offsetof(struct widget_instance, animation) == 0x50 ? 1 : -1]; + +/* menu_tags.c's */ +struct pc_menu_setting +{ + long definition_index; + char const *setting; + long value_count; + char const *values[MAXIMUM_STRINGS]; + short loaded_index; +}; + +struct pc_menu_setting *pc_menu_setting_get(long definition_index); + +/* ---------- private code */ + +static boolean campaign_profile(short controller, struct player_profile *profile); + +static boolean text_is_number(char const *text, double *number) +{ + char *end; + + *number = strtod(text, &end); + return *text && !*end; +} + +/* whether the widget is one of a spinner's items, which the game makes of +its strings from its own definition, handlers and all (ui_widget.c, +ui_widget_load_children_recursive) */ +static boolean spinner_item(struct widget_instance const *widget) +{ + return widget->parent && widget->parent->definition_tag_index == widget->definition_tag_index; +} + +/* the value shown for the setting's: the same one, else the nearest number */ +static short setting_value_index(struct pc_menu_setting const *setting, char const *current) +{ + double current_number, value_number, distance = 0.0; + short index, nearest = 0; + + for (index = 0; index < setting->value_count; index++) + { + if (!_stricmp(setting->values[index], current)) + return index; + } + if (!text_is_number(current, ¤t_number)) + return 0; + for (index = 0; index < setting->value_count; index++) + { + if (text_is_number(setting->values[index], &value_number)) + { + double gap = value_number > current_number ? value_number - current_number : current_number - value_number; + + if (index == 0 || gap < distance) + { + distance = gap; + nearest = index; + } + } + } + return nearest; +} + +/* the profile's settings ("profile."): those of the controller in +the profile being edited */ +static struct +{ + char const *name; + short field; + /* its default (player_profile.c's new profile's) */ + byte default_value; +} const profile_settings[] = +{ + { "profile.look_sensitivity", 0, 3 }, + { "profile.invert_look", 1, FALSE }, + { "profile.flight_inversion", 2, FALSE }, + { "profile.autocenter", 3, FALSE }, + { "profile.button_preset", 4, 0 }, + { "profile.joystick_preset", 5, 0 }, + { "profile.vibration", 6, TRUE }, + { "profile.ingame_help", 7, TRUE }, +}; + +static byte *profile_setting_field(struct player_profile *profile, short field) +{ + struct player_profile_controller_settings *controls = &profile->controller_settings; + + switch (field) + { + case 0: return &controls->look_sensitivity; + case 1: return (byte *)&controls->invert_look; + case 2: return (byte *)&controls->flight_stick_aircraft_controls; + case 3: return (byte *)&controls->autocenter; + case 4: return &controls->button_preset; + case 5: return &controls->joystick_preset; + case 6: return (byte *)&controls->vibration_disabled; + case 7: return (byte *)&controls->ingame_help_disabled; + } + return NULL; +} + +static boolean profile_setting_boolean(short field) +{ + return field != 0 && field != 4 && field != 5; +} + +/* a setting's value as text, or (default_value) its default: config.toml's, +or the profile's */ +static boolean setting_text(char const *name, char *text, unsigned int size, boolean default_value) +{ + short index; + + if (strncmp(name, "profile.", 8)) + { + if (!(default_value ? config_default(name, text, size) : config_text(name, text, size))) + return FALSE; + /* (display.mode empty: display.fullscreen's, as the window has it: + sdl_platform.c) */ + if (!strcmp(name, "display.mode") && !text[0]) + snprintf(text, size, "%s", default_value || config_boolean("display.fullscreen") ? "borderless" : "windowed"); + return TRUE; + } + for (index = 0; index < NUMBEROF(profile_settings); index++) + { + struct player_profile *profile = player_ui_get_edit_player_profile(); + short field = profile_settings[index].field; + long value; + + if (strcmp(name, profile_settings[index].name)) + continue; + if (default_value) + value = profile_settings[index].default_value; + else if (profile && profile_setting_field(profile, field)) + value = *profile_setting_field(profile, field); + else + return FALSE; + /* (vibration and help are kept as their opposites) */ + if (!default_value && (field == 6 || field == 7)) + value = !value; + if (profile_setting_boolean(field)) + snprintf(text, size, "%s", value ? "true" : "false"); + else + snprintf(text, size, "%ld", value); + return TRUE; + } + return FALSE; +} + +static boolean setting_write(char const *name, char const *value) +{ + short index; + + if (strncmp(name, "profile.", 8)) + return config_write(name, value); + for (index = 0; index < NUMBEROF(profile_settings); index++) + { + struct player_profile *profile = player_ui_get_edit_player_profile(); + short field = profile_settings[index].field; + byte *place; + long number; + + if (strcmp(name, profile_settings[index].name)) + continue; + if (!profile || !(place = profile_setting_field(profile, field))) + return FALSE; + number = profile_setting_boolean(field) ? !strcmp(value, "true") : atol(value); + if (field == 6 || field == 7) + number = !number; + *place = (byte)number; + return TRUE; + } + return FALSE; +} + +static boolean setting_load(struct widget_instance *widget) +{ + struct pc_menu_setting *setting = pc_menu_setting_get(widget->definition_tag_index); + char current[300]; + + if (spinner_item(widget)) + return TRUE; + if (!setting || !setting_text(setting->setting, current, sizeof(current), FALSE)) + return FALSE; + setting->loaded_index = setting_value_index(setting, current); + if (setting->loaded_index < (short)widget->parameters.list.number_of_items) + widget->parameters.list.selected_index = setting->loaded_index; + return TRUE; +} + +static boolean setting_save(struct widget_instance *widget) +{ + struct pc_menu_setting *setting = pc_menu_setting_get(widget->definition_tag_index); + + if (spinner_item(widget)) + return TRUE; + if (!setting || setting->loaded_index == NONE || widget->parameters.list.selected_index < 0 || + widget->parameters.list.selected_index >= setting->value_count) + return FALSE; + if (widget->parameters.list.selected_index == setting->loaded_index) + return TRUE; + if (!config_write(setting->setting, setting->values[widget->parameters.list.selected_index])) + { + platform_log("menus: could not write %s to config.toml", setting->setting); + return FALSE; + } + platform_log("menus: %s = %s (from the next start)", setting->setting, setting->values[widget->parameters.list.selected_index]); + setting->loaded_index = widget->parameters.list.selected_index; + return TRUE; +} + +static short controller_of(struct widget_instance const *widget) +{ + return widget->local_player_index >= 0 && widget->local_player_index < 4 ? widget->local_player_index : 0; +} + +/* the controller the event came from, else the widget's */ +static short event_controller(struct widget_instance const *widget, struct event_record const *event) +{ + return event && event->controller_index >= 0 && event->controller_index < 4 ? + event->controller_index : controller_of(widget); +} + +/* runs the custom activation handler of the widget's list, or the nearest +of its parents' that has one (widget_deleted: the widget went with its +screen) */ +static boolean item_activated(struct widget_instance *widget, short controller, boolean *widget_deleted) +{ + struct widget_instance *parent; + + for (parent = widget->parent; parent; parent = parent->parent) + { + boolean deleted; + + if (ui_widget_port_dispatch_event(parent, EVENT_CUSTOM_ACTIVATION, controller, &deleted)) + { + if (deleted) + *widget_deleted = TRUE; + return TRUE; + } + } + return FALSE; +} + +/* whether the multiplayer menu's choice was LAN, not Internet (mp type set +mode) */ +static boolean lan_mode; + +/* hides the descendants of the widget with the name */ +static void hide_named(struct widget_instance *widget, char const *name) +{ + struct widget_instance *child; + + for (child = widget->child; child; child = child->next) + { + if (!strcmp(child->name, name)) + child->visible = FALSE; + hide_named(child, name); + } +} + +/* the server browser's panels for errors and filters are hidden until they +are wanted, and its title is Internet's or LAN's (gamespy screen init) */ +static void server_browser_initialize(struct widget_instance *screen) +{ + struct widget_instance *child; + + hide_named(screen, lan_mode ? "header_internet" : "header_lan"); + /* (a widget's name is its definition's: the last part of ours) */ + for (child = screen->child; child; child = child->next) + { + if (!strcmp(child->name, "gamespy_error_fullscreen") || !strcmp(child->name, "filters_screen")) + { + child->visible = FALSE; + if (screen->focused_child == child) + screen->focused_child = NULL; + } + } + for (child = screen->child; child && !screen->focused_child; child = child->next) + { + if (child->visible && child->type == 3) + screen->focused_child = child; + } +} + +/* begins editing player 1's profile (as the campaign has it); FALSE if +there is none */ +boolean pc_menu_profile_edit_begin(void) +{ + struct player_profile profile; + + if (!campaign_profile(0, &profile)) + return FALSE; + player_ui_begin_editing_profile(player_ui_get_active_player_profile_index(0)); + return TRUE; +} + +/* ---------- the campaign */ + +/* the rows of its lists (main_menu/new_select/list_item_0 to 10) */ +#define MAXIMUM_ROWS 11 +/* its strings' and sp_levels' entries past the levels': a level not +reached, and (map_data) a saved game on the level */ +#define LEVEL_UNAVAILABLE 10 +#define LEVEL_IN_PROGRESS 11 +#define ROW_TEXT_LENGTH 64 +#define MAXIMUM_PROFILES 32 +#define SOUND_ERROR 4 +#define SOUND_FORWARD 2 + +/* (an index whose profile reads: saved_game_files.c's) */ +#define PROFILE_VALID_BIT 0x80000000UL + +struct campaign_level +{ + boolean available; + /* finished on Normal, Heroic, Legendary (difficulty_options_small's + frames 1 to 3) */ + boolean finished[3]; +}; + +struct campaign_saved_game +{ + long profile_index; + wchar_t profile_name[MAXIMUM_PLAYER_PROFILE_NAME_LENGTH + 1]; + short level; + short difficulty; +}; + +static struct +{ + struct campaign_level levels[NUMBER_OF_SINGLE_PLAYER_LEVELS]; + /* player 1's saved game's level, else NONE */ + short saved_level; + /* the level and saved game the lists' descriptions last showed */ + short shown_level; + short shown_saved_game; + /* Load Game's */ + struct campaign_saved_game saved_games[MAXIMUM_ROWS]; + short saved_game_count; + short saved_game_to_delete; +} campaign; + +/* the descendant of the widget with the name (a widget's name is its +definition's: the last part of ours), the nth of them */ +static struct widget_instance *descendant(struct widget_instance *widget, char const *name, long *nth) +{ + struct widget_instance *child; + + for (child = widget->child; child; child = child->next) + { + struct widget_instance *found; + + if (!strcmp(child->name, name) && (*nth)-- == 0) + return child; + found = descendant(child, name, nth); + if (found) + return found; + } + return NULL; +} + +static struct widget_instance *named(struct widget_instance *widget, char const *name, long nth) +{ + return widget ? descendant(widget, name, &nth) : NULL; +} + +/* the text box's own text (as the server list sets its items'), in a +buffer of length characters (made the first time: a text box is always +given the same length) */ +static void text_set_length(struct widget_instance *text_box, wchar_t const *text, short length) +{ + if (!text_box) + return; + if (!text_box->parameters.text_box.text) + { + text_box->parameters.text_box.text = ui_widget_realloc(NULL, length * sizeof(wchar_t), + __FILE__, __LINE__); + } + if (text_box->parameters.text_box.text) + { + ustrncpy(text_box->parameters.text_box.text, text, length - 1); + text_box->parameters.text_box.text[length - 1] = 0; + } +} + +static void text_set(struct widget_instance *text_box, wchar_t const *text) +{ + text_set_length(text_box, text, ROW_TEXT_LENGTH); +} + +/* one of our string lists' strings, on one line (its line breaks spaces) */ +static void string_get(char const *list, short index, wchar_t *text) +{ + long tag_index = tag_loaded('ustr', list); + wchar_t const *string = tag_index != NONE ? unicode_string_list_get_string(tag_index, index) : NULL; + long length = 0; + + for (; string && *string && length < ROW_TEXT_LENGTH - 1; string++) + { + wchar_t character = *string == '\r' || *string == '\n' ? ' ' : *string; + + if (character != ' ' || (length && text[length - 1] != ' ')) + text[length++] = character; + } + while (length && text[length - 1] == ' ') + length--; + text[length] = 0; +} + +/* the list's row that has the focus, else NONE (its buttons) */ +static short focused_row(struct widget_instance *list) +{ + struct widget_instance *child; + short index = 0; + + for (child = list->child; child && index < MAXIMUM_ROWS; child = child->next, index++) + { + if (child == list->focused_child) + return strncmp(child->name, "list_item_", 10) ? NONE : index; + } + return NONE; +} + +static void focus_row(struct widget_instance *list, short row) +{ + struct widget_instance *child = list->child; + short index; + + for (index = 0; child && child->next && index < row; index++) + child = child->next; + if (row == NONE) + { + /* (its buttons: the last child) */ + while (child && child->next) + child = child->next; + } + if (child) + { + list->focused_child = child; + list->parameters.list.selected_index = row == NONE ? 0 : row; + } +} + +/* shows the first count rows, with their texts, the arrows on the one that +has the focus, and no scroll buttons (the rows are enough) */ +static void rows_update(struct widget_instance *list, short count, void (*row_text)(short row, wchar_t *text)) +{ + struct widget_instance *row; + short index = 0; + + for (row = list->child; row && index < MAXIMUM_ROWS; row = row->next, index++) + { + wchar_t text[ROW_TEXT_LENGTH]; + struct widget_instance *arrows = named(row, "list_item_arrows", 0); + + if (strncmp(row->name, "list_item_", 10)) + break; + row->visible = index < count; + if (index < count) + { + row_text(index, text); + text_set(named(row, "list_item_text", 0), text); + } + if (arrows) + arrows->visible = row == list->focused_child; + if (named(row, "scroll_up_button", 0)) + named(row, "scroll_up_button", 0)->visible = FALSE; + if (named(row, "scroll_down_button", 0)) + named(row, "scroll_down_button", 0)->visible = FALSE; + } +} + +/* player 1's profile, read again (the active one, else the one last used, +else the first), on the controller; FALSE if there is none */ +static boolean campaign_profile(short controller, struct player_profile *profile) +{ + long profile_index = player_ui_get_active_player_profile_index(0); + + if (profile_index == NONE || !player_profile_get(profile_index, profile)) + { + profile_index = player_ui_get_player1_last_used_profile_index(); + if (profile_index == NONE || !player_profile_get(profile_index, profile)) + { + word count = 1; + + profile_index = NONE; + player_profiles_enumerate_available_to_local_player_index(NONE, &count, &profile_index, FALSE); + if ((short)count <= 0 || profile_index == NONE || !player_profile_get(profile_index, profile)) + return FALSE; + } + } + player_ui_set_active_player_profile(0, profile_index, profile); + player_ui_set_single_player_local_player_controller(0, controller); + return TRUE; +} + +/* the levels the profile has reached (as the Xbox's list has them: those +it has played, the one after the last it finished, the first) and finished, +and its saved game's */ +static void campaign_levels_read(struct player_profile const *profile) +{ + char const *map_name; + short highest_level, highest_difficulty, difficulty, level; + + player_profile_get_highest_completed_solo_level((struct player_profile *)profile, &highest_level, + &highest_difficulty); + for (level = 0; level < NUMBER_OF_SINGLE_PLAYER_LEVELS; level++) + { + byte flags = profile->single_player_map_flags[level]; + short marker; + + campaign.levels[level].available = flags || level == highest_level + 1 || level == 0; + for (marker = 0; marker < 3; marker++) + campaign.levels[level].finished[marker] = (flags >> (marker + 1)) & 1; + } + if (!ui_widget_port_saved_game(&map_name, &campaign.saved_level, &difficulty)) + campaign.saved_level = NONE; + (void)map_name; +} + +/* plays the map, at the difficulty (a saved game in it goes on: main.c's +main_new_map, if its difficulty is this one) */ +static void campaign_start(char const *map_name, short difficulty, short controller) +{ + player_ui_set_single_player_local_player_controller(0, controller); + main_set_map_name(map_name); + main_set_difficulty(difficulty); + game_connection_set(0); + main_menu_switch_to_single_player(); + player_ui_remember_player1_profile(TRUE); + ui_play_audio_feedback_sound(SOUND_FORWARD); +} + +static boolean campaign_fail(void) +{ + ui_play_audio_feedback_sound(SOUND_ERROR); + return FALSE; +} + +/* the description's level: its name, picture and words, and the +difficulties it has been finished on (finished: New Game's, NULL for a level +not reached), or (saved_difficulty, not NONE: Load Game's) the saved game's +difficulty */ +static void level_description(struct widget_instance *description, char const *prefix, short level, boolean in_progress, + boolean const *finished, short saved_difficulty) +{ + char name[64]; + struct widget_instance *widget; + short marker; + + snprintf(name, sizeof(name), "%s_right_name", prefix); + if ((widget = named(description, name, 0)) != NULL) + widget->parameters.text_box.string_list_index = level; + snprintf(name, sizeof(name), "%s_right_pic", prefix); + if ((widget = named(description, name, 0)) != NULL) + widget->animation.current_frame_index = level; + snprintf(name, sizeof(name), "%s_right_data", prefix); + if ((widget = named(description, name, 0)) != NULL && saved_difficulty != NONE) + { + wchar_t text[ROW_TEXT_LENGTH]; + + string_get("pc\\main_menu\\player_profiles_select\\difficulty_names", saved_difficulty, text); + text_set(widget, text); + } + else if (widget) + widget->parameters.text_box.string_list_index = in_progress ? LEVEL_IN_PROGRESS : level; + for (marker = 0; marker < 3; marker++) + { + if ((widget = named(description, "difficulty_indicator", marker)) != NULL) + { + widget->visible = saved_difficulty != NONE ? marker == 0 : finished && finished[marker]; + widget->animation.current_frame_index = saved_difficulty != NONE ? saved_difficulty : marker + 1; + } + } +} + +/* player 1's profile's name (the active one, else the one used last: the +main menu clears the active ones), on the descriptions' +current_profile_name */ +static void profile_name_show(struct widget_instance *description) +{ + /* (the profile read from its file at most once a second) */ + static struct player_profile profile; + static long read_index = NONE; + static unsigned long read_time; + long index = player_ui_get_active_player_profile_index(0); + + if (!description) + return; + if (index != NONE) + { + player_ui_get_active_player_profile(0, &profile); + read_index = NONE; + } + else if ((index = player_ui_get_player1_last_used_profile_index()) == NONE) + return; + else if (index != read_index || system_milliseconds() - read_time > 1000) + { + read_index = NONE; + if (!player_profile_get(index, &profile)) + return; + read_index = index; + read_time = system_milliseconds(); + } + text_set(named(description, "current_profile_name", 0), profile.player_name); +} + +/* (Campaign's menu) "campaign menu init": player 1's profile and saved +game, for its items */ +static boolean campaign_menu_initialize(short controller) +{ + struct player_profile profile; + + if (campaign_profile(controller, &profile)) + campaign_levels_read(&profile); + return TRUE; +} + +/* "campaign menu continue": the saved game goes on */ +static boolean campaign_continue(short controller) +{ + struct player_profile profile; + char const *map_name; + short level, difficulty; + + if (!campaign_profile(controller, &profile) || !ui_widget_port_saved_game(&map_name, &level, &difficulty)) + return campaign_fail(); + campaign_start(map_name, difficulty, controller); + return TRUE; +} + +/* New Game's list: "initialize sp level list solo" starts on the level last +played */ +static boolean level_list_initialize(struct widget_instance *list, short controller) +{ + struct player_profile profile; + short level; + + if (!campaign_profile(controller, &profile)) + return FALSE; + campaign_levels_read(&profile); + level = PIN(profile.last_single_player_map_played, 0, NUMBER_OF_SINGLE_PLAYER_LEVELS - 1); + if (campaign.saved_level != NONE) + level = campaign.saved_level; + if (!campaign.levels[level].available) + level = 0; + campaign.shown_level = level; + focus_row(list, level); + return TRUE; +} + +static void level_row_text(short row, wchar_t *text) +{ + string_get("pc\\main_menu\\map_list", campaign.levels[row].available ? row : LEVEL_UNAVAILABLE, text); +} + +/* "solo map list update" */ +static void level_list_update(struct widget_instance *list) +{ + short row = focused_row(list), level; + + rows_update(list, NUMBER_OF_SINGLE_PLAYER_LEVELS, level_row_text); + if (row != NONE && row < NUMBER_OF_SINGLE_PLAYER_LEVELS) + campaign.shown_level = row; + level = campaign.shown_level; + if (!campaign.levels[level].available) + level = LEVEL_UNAVAILABLE; + level_description(list->parameters.list.extended_description, "replay_level", level, + level == campaign.saved_level, level == LEVEL_UNAVAILABLE ? NULL : campaign.levels[level].finished, NONE); + profile_name_show(list->parameters.list.extended_description); +} + +/* "solo level set map": the level shown, if reached, for the difficulty +menu */ +static boolean level_choose(void) +{ + short level = campaign.shown_level; + + if (level < 0 || level >= NUMBER_OF_SINGLE_PLAYER_LEVELS || !campaign.levels[level].available) + return campaign_fail(); + /* (not yet: setting it at the main menu changes map, as the Xbox's has it + not) */ + main_set_map_name(main_get_solo_level_name(level)); + main_defer_map_map_change(); + return TRUE; +} + +/* the difficulty menu: "difficulty item select" starts the game at the +item's; "set difficulty" (its OK button) at the one its description shows */ +static boolean difficulty_start(short difficulty, short controller) +{ + struct player_profile profile; + char const *map_name = main_get_map_name(); + + if (!campaign_profile(controller, &profile)) + return campaign_fail(); + if (!map_name || main_get_solo_level_from_name(map_name) == NONE) + map_name = main_get_solo_level_name(0); + campaign_start(map_name, PIN(difficulty, 0, 3), controller); + return TRUE; +} + +static short difficulty_shown(struct widget_instance *widget) +{ + for (; widget; widget = widget->parent) + { + if (!strcmp(widget->name, "difficulty_select_list") && widget->parameters.list.extended_description) + { + struct widget_instance *picture = named(widget->parameters.list.extended_description, + "difficulty_options", 0); + + return picture ? picture->animation.current_frame_index : main_get_difficulty(); + } + } + return main_get_difficulty(); +} + +static short sibling_index(struct widget_instance *widget) +{ + struct widget_instance *child; + short index = 0; + + for (child = widget->parent ? widget->parent->child : widget; child && child != widget; child = child->next) + index++; + return index; +} + +/* Load Game's list: each profile's saved game (read with the profile as +player 1's, which then is again) */ +static void saved_games_read(short controller) +{ + struct player_profile profile, active; + long profiles[MAXIMUM_PROFILES]; + long active_index; + word count = MAXIMUM_PROFILES; + short index; + + campaign.saved_game_count = 0; + if (!campaign_profile(controller, &active)) + return; + active_index = player_ui_get_active_player_profile_index(0); + player_profiles_enumerate_available_to_local_player_index(NONE, &count, profiles, FALSE); + for (index = 0; index < (short)count && campaign.saved_game_count < MAXIMUM_ROWS; index++) + { + struct campaign_saved_game *saved_game = &campaign.saved_games[campaign.saved_game_count]; + char const *map_name; + + if (!((unsigned long)profiles[index] & PROFILE_VALID_BIT) || !player_profile_get(profiles[index], &profile)) + continue; + player_ui_set_active_player_profile(0, profiles[index], &profile); + if (ui_widget_port_saved_game(&map_name, &saved_game->level, &saved_game->difficulty)) + { + saved_game->profile_index = profiles[index]; + ustrncpy(saved_game->profile_name, profile.player_name, MAXIMUM_PLAYER_PROFILE_NAME_LENGTH); + saved_game->profile_name[MAXIMUM_PLAYER_PROFILE_NAME_LENGTH] = 0; + campaign.saved_game_count++; + } + } + player_ui_set_active_player_profile(0, active_index, &active); + campaign_levels_read(&active); +} + +/* "load game menu init" */ +static boolean saved_game_list_initialize(struct widget_instance *list, short controller) +{ + short index; + + saved_games_read(controller); + campaign.shown_saved_game = 0; + for (index = 0; index < campaign.saved_game_count; index++) + { + if (campaign.saved_games[index].profile_index == player_ui_get_active_player_profile_index(0)) + campaign.shown_saved_game = index; + } + focus_row(list, campaign.saved_game_count ? campaign.shown_saved_game : NONE); + return TRUE; +} + +static void saved_game_row_text(short row, wchar_t *text) +{ + ustrncpy(text, campaign.saved_games[row].profile_name, ROW_TEXT_LENGTH - 1); + text[ROW_TEXT_LENGTH - 1] = 0; +} + +/* "load game list update" */ +static void saved_game_list_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + struct widget_instance *item = named(description, "load_level_right_item", 0); + short row = focused_row(list); + + rows_update(list, campaign.saved_game_count, saved_game_row_text); + if (row != NONE && row < campaign.saved_game_count) + campaign.shown_saved_game = row; + if (campaign.shown_saved_game >= campaign.saved_game_count) + campaign.shown_saved_game = campaign.saved_game_count - 1; + if (item) + item->visible = campaign.saved_game_count > 0; + if (campaign.saved_game_count > 0) + { + struct campaign_saved_game const *saved_game = &campaign.saved_games[campaign.shown_saved_game]; + + level_description(description, "load_level", saved_game->level, FALSE, NULL, saved_game->difficulty); + } + else if (row != NONE) + { + focus_row(list, NONE); + } + profile_name_show(description); +} + +/* "load game menu activated": the saved game goes on, with its profile as +player 1's */ +static boolean saved_game_continue(short controller) +{ + struct campaign_saved_game const *saved_game; + struct player_profile profile; + + if (campaign.shown_saved_game < 0 || campaign.shown_saved_game >= campaign.saved_game_count) + return campaign_fail(); + saved_game = &campaign.saved_games[campaign.shown_saved_game]; + if (!player_profile_get(saved_game->profile_index, &profile)) + return campaign_fail(); + player_ui_set_active_player_profile(0, saved_game->profile_index, &profile); + campaign_start(main_get_solo_level_name(saved_game->level), saved_game->difficulty, controller); + return TRUE; +} + +/* "load game menu delete request" (before its question) and "delete +finish" (its OK): the saved game shown goes */ +static boolean saved_game_delete_request(void) +{ + if (campaign.shown_saved_game < 0 || campaign.shown_saved_game >= campaign.saved_game_count) + return campaign_fail(); + campaign.saved_game_to_delete = campaign.shown_saved_game; + return TRUE; +} + +static boolean saved_game_delete(short controller) +{ + char path[256]; + + if (campaign.saved_game_to_delete < 0 || campaign.saved_game_to_delete >= campaign.saved_game_count || + !player_profile_get_enclosing_directory_path(campaign.saved_games[campaign.saved_game_to_delete].profile_index, + path) || strlen(path) + strlen("savegame.bin") >= sizeof(path)) + { + return campaign_fail(); + } + strcat(path, "savegame.bin"); + if (!DeleteFileA(path)) + platform_log("menus: could not delete %s", path); + campaign.saved_game_to_delete = NONE; + saved_games_read(controller); + return TRUE; +} + +/* the settings' screens (tools/port_settings.py): every spinner of a +setting in the screen */ +static void settings_each(struct widget_instance *widget, boolean (*visit)(struct widget_instance *spinner, + struct pc_menu_setting *setting)) +{ + struct widget_instance *child; + + for (child = widget->child; child; child = child->next) + { + struct pc_menu_setting *setting = child->type == 2 && !spinner_item(child) ? + pc_menu_setting_get(child->definition_tag_index) : NULL; + + if (setting) + visit(child, setting); + else + settings_each(child, visit); + } +} + +static struct widget_instance *screen_of(struct widget_instance *widget) +{ + while (widget->parent) + widget = widget->parent; + return widget; +} + +/* "port settings save" (OK): those changed written, and applied */ +static boolean setting_changed_save(struct widget_instance *spinner, struct pc_menu_setting *setting) +{ + short index = spinner->parameters.list.selected_index; + + if (index < 0 || index >= setting->value_count || index == setting->loaded_index) + return TRUE; + if (!setting_write(setting->setting, setting->values[index])) + { + platform_log("menus: could not set %s", setting->setting); + return FALSE; + } + platform_log("menus: %s = %s", setting->setting, setting->values[index]); + setting->loaded_index = index; + return TRUE; +} + +/* "port settings defaults" */ +static boolean setting_default_show(struct widget_instance *spinner, struct pc_menu_setting *setting) +{ + char text[300]; + + if (setting_text(setting->setting, text, sizeof(text), TRUE)) + spinner->parameters.list.selected_index = setting_value_index(setting, text); + return TRUE; +} + +/* "port settings help": the line of the row chosen (by its label's string: +the buttons' is the first) */ +static void settings_help(struct widget_instance *list) +{ + struct widget_instance *help = list->parameters.list.extended_description; + struct widget_instance *row = list->focused_child; + short index = 0; + + if (!help) + return; + if (row && row->child && row->child->type == 1 && strncmp(row->name, "button", 6)) + { + short label = pc_menu_string_index(row->child->definition_tag_index); + + if (label != NONE) + index = label + 1; + } + help->parameters.text_box.string_list_index = index; +} + +/* ---------- Change Color: the profile's colour, from a list of the +game's colours (more than its rows: it scrolls), by the Xbox's names for +its spinner's functions */ + +#define COLOR_COUNT 18 +#define COLOR_ROWS 11 + +static struct +{ + /* the colour on the first row, and the colour chosen */ + short first; + short color; +} color_list; + +static void color_row_text(short row, wchar_t *text) +{ + string_get("pc\\main_menu\\settings_select\\player_setup\\player_profile_edit\\color_edit\\colors_list", + (short)(color_list.first + row), text); +} + +/* "color picker menu initialize": on the profile's colour */ +static boolean color_list_initialize(struct widget_instance *list) +{ + struct player_profile *profile = player_ui_get_edit_player_profile(); + + if (!profile) + return FALSE; + color_list.color = (short)PIN(profile->primary_color_index, 0, COLOR_COUNT - 1); + color_list.first = (short)PIN(color_list.color - COLOR_ROWS / 2, 0, COLOR_COUNT - COLOR_ROWS); + focus_row(list, (short)(color_list.color - color_list.first)); + return TRUE; +} + +/* a list of more items than its rows: the row chosen kept off its ends +while there are more past them, the list moving instead; the item chosen, +or NONE (its buttons) */ +static short list_scroll(struct widget_instance *list, short *first, short count, short rows) +{ + short row = focused_row(list); + + if (row == rows - 1 && *first + rows < count) + { + (*first)++; + focus_row(list, --row); + } + else if (row == 0 && *first > 0) + { + (*first)--; + focus_row(list, ++row); + } + return row == NONE || row >= rows ? NONE : (short)(*first + row); +} + +/* "color picker update": the list scrolled on at its ends, the colour's +name and picture */ +static void color_list_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + struct widget_instance *widget; + short color = list_scroll(list, &color_list.first, COLOR_COUNT, COLOR_ROWS); + + if (color != NONE) + color_list.color = color; + rows_update(list, COLOR_ROWS, color_row_text); + if ((widget = named(description, "color_right_name", 0)) != NULL) + widget->parameters.text_box.string_list_index = color_list.color; + if ((widget = named(description, "color_right_pic", 0)) != NULL) + widget->animation.current_frame_index = color_list.color; + profile_name_show(description); +} + +/* "color picker select color" */ +static boolean color_choose(void) +{ + struct player_profile *profile = player_ui_get_edit_player_profile(); + + if (!profile) + return FALSE; + profile->primary_color_index = color_list.color; + return TRUE; +} + +/* ---------- Profiles: the player profiles, and a row to make one (as the +PC version's list has them, but by the Xbox's names for its spinner's +functions); choosing one makes it player 1's, the profile the campaign and +Settings use; deleting one asks first */ + +#define PROFILE_ROWS 11 +#define PROFILE_DEFAULT_BIT 0x40000000UL + +static struct +{ + long indices[MAXIMUM_PROFILES]; + struct player_profile profiles[MAXIMUM_PROFILES]; + /* the profiles; the row after them makes a new one */ + short count; + short first; + short chosen; + long to_delete; +} profile_list; + +/* the profiles, read again when they are not those read last */ +static void profile_list_read(boolean always) +{ + long indices[MAXIMUM_PROFILES]; + word count = MAXIMUM_PROFILES; + short index, valid = 0; + + player_profiles_enumerate_available_to_local_player_index(NONE, &count, indices, FALSE); + for (index = 0; index < (short)count; index++) + { + if ((unsigned long)indices[index] & PROFILE_VALID_BIT) + indices[valid++] = indices[index]; + } + if (!always && valid == profile_list.count && + !memcmp(indices, profile_list.indices, valid * sizeof(indices[0]))) + { + return; + } + profile_list.count = 0; + for (index = 0; index < valid; index++) + { + if (player_profile_get(indices[index], &profile_list.profiles[profile_list.count])) + profile_list.indices[profile_list.count++] = indices[index]; + } + profile_list.chosen = (short)PIN(profile_list.chosen, 0, profile_list.count); +} + +static short profile_list_rows(void) +{ + return (short)MIN(profile_list.count + 1, PROFILE_ROWS); +} + +/* "player profile list initialize": on player 1's profile */ +static boolean profile_list_initialize(struct widget_instance *list) +{ + long active = player_ui_get_active_player_profile_index(0); + short index; + + profile_list_read(TRUE); + if (active == NONE) + active = player_ui_get_player1_last_used_profile_index(); + profile_list.chosen = 0; + for (index = 0; index < profile_list.count; index++) + { + if (profile_list.indices[index] == active) + profile_list.chosen = index; + } + profile_list.first = (short)PIN(profile_list.chosen - PROFILE_ROWS / 2, 0, + MAX(0, profile_list.count + 1 - PROFILE_ROWS)); + focus_row(list, (short)(profile_list.chosen - profile_list.first)); + return TRUE; +} + +static void profile_row_text(short row, wchar_t *text) +{ + short item = (short)(profile_list.first + row); + + if (item < profile_list.count) + { + ustrncpy(text, profile_list.profiles[item].player_name, MAXIMUM_PLAYER_PROFILE_NAME_LENGTH); + text[MAXIMUM_PLAYER_PROFILE_NAME_LENGTH] = 0; + } + else + { + string_get("pc\\main_menu\\player_profiles_select\\profile_description_labels", 4, text); + } +} + +static void visible_set(struct widget_instance *widget, boolean visible) +{ + if (widget) + widget->visible = visible; +} + +/* "3wide player profile list update": the rows, and the profile chosen: +its name, colour, current level, best difficulty and controls */ +static void profile_list_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + struct widget_instance *widget; + short item; + boolean profile; + + profile_list_read(FALSE); + item = list_scroll(list, &profile_list.first, (short)(profile_list.count + 1), profile_list_rows()); + if (item != NONE) + profile_list.chosen = item; + rows_update(list, profile_list_rows(), profile_row_text); + profile = profile_list.chosen < profile_list.count; + text_set(named(description, "player_profile_right_name", 0), + profile ? profile_list.profiles[profile_list.chosen].player_name : L""); + visible_set(named(description, "empty_profile_slot_text", 0), !profile); + visible_set(named(description, "qtr_screen_profile_color_pic", 0), profile); + visible_set(named(description, "current_level_label", 0), profile); + visible_set(named(description, "current_level", 0), profile); + visible_set(named(description, "joystick_controls_label", 0), profile); + visible_set(named(description, "joystick_controls", 0), profile); + visible_set(named(description, "skill_level_label", 0), FALSE); + visible_set(named(description, "skill_level", 0), FALSE); + if (profile) + { + struct player_profile *chosen = &profile_list.profiles[profile_list.chosen]; + short level, difficulty; + + if ((widget = named(description, "qtr_screen_profile_color_pic", 0)) != NULL) + widget->animation.current_frame_index = (short)PIN(chosen->primary_color_index, 0, COLOR_COUNT - 1); + if ((widget = named(description, "current_level", 0)) != NULL) + { + widget->parameters.text_box.string_list_index = + (short)PIN(chosen->last_single_player_map_played, 0, NUMBER_OF_SINGLE_PLAYER_LEVELS - 1); + } + if ((widget = named(description, "joystick_controls", 0)) != NULL) + widget->parameters.text_box.string_list_index = chosen->controller_settings.invert_look ? 1 : 0; + player_profile_get_highest_completed_solo_level(chosen, &level, &difficulty); + if (level != NONE && (widget = named(description, "skill_level", 0)) != NULL) + { + widget->parameters.text_box.string_list_index = difficulty; + widget->visible = TRUE; + visible_set(named(description, "skill_level_label", 0), TRUE); + } + } + profile_name_show(description); +} + +/* "profile manager select": the profile chosen made player 1's (and the +one used last); FALSE for the row that makes one (its handler then asks) */ +static boolean profile_choose(short controller) +{ + struct player_profile profile; + + if (profile_list.chosen >= profile_list.count || + !player_profile_get(profile_list.indices[profile_list.chosen], &profile)) + { + return FALSE; + } + player_ui_set_active_player_profile(0, profile_list.indices[profile_list.chosen], &profile); + player_ui_set_single_player_local_player_controller(0, controller); + player_ui_remember_player1_profile(TRUE); + return TRUE; +} + +/* "request del player profile" (before its question) */ +static boolean profile_delete_request(void) +{ + if (profile_list.chosen >= profile_list.count) + return campaign_fail(); + profile_list.to_delete = profile_list.indices[profile_list.chosen]; + return TRUE; +} + +/* "final del player profile" (its OK): deleted, and if it was player 1's, +the first left is */ +static boolean profile_delete(void) +{ + long index = profile_list.to_delete; + boolean active = index == player_ui_get_active_player_profile_index(0); + + profile_list.to_delete = NONE; + if (index == NONE || ((unsigned long)index & PROFILE_DEFAULT_BIT)) + return campaign_fail(); + player_profile_delete(index); + profile_list_read(TRUE); + if (active) + { + struct player_profile profile; + + if (profile_list.count && player_profile_get(profile_list.indices[0], &profile)) + { + player_ui_set_active_player_profile(0, profile_list.indices[0], &profile); + player_ui_remember_player1_profile(TRUE); + } + else + { + memset(&profile, 0, sizeof(profile)); + player_ui_set_active_player_profile(0, NONE, &profile); + } + } + return TRUE; +} + +/* ---------- Controls Setup: the keyboard and mouse's controls +(port/linux/include/halo_keyboard.h), shown a group at a time, two +bindings each */ + +#define CONTROL_BINDINGS 2 +#define CONTROL_ROWS 7 +#define CONTROL_NAME_LENGTH 32 + +static struct +{ + char const *setting; + wchar_t const *label; + short group; +} const controls[] = +{ + { "controls.move_forward", L"MOVE FORWARD", 0 }, + { "controls.move_backward", L"MOVE BACKWARD", 0 }, + { "controls.strafe_left", L"STRAFE LEFT", 0 }, + { "controls.strafe_right", L"STRAFE RIGHT", 0 }, + { "controls.jump", L"JUMP", 0 }, + { "controls.crouch", L"CROUCH", 0 }, + { "controls.fire", L"FIRE", 1 }, + { "controls.throw_grenade", L"THROW GRENADE", 1 }, + { "controls.melee", L"MELEE", 1 }, + { "controls.reload", L"RELOAD", 1 }, + { "controls.zoom", L"ZOOM", 1 }, + { "controls.switch_weapon", L"SWITCH WEAPON", 1 }, + { "controls.switch_grenade", L"SWITCH GRENADE", 1 }, + { "controls.action", L"ACTION", 2 }, + { "controls.flashlight", L"FLASHLIGHT", 2 }, + { "controls.scoreboard", L"SHOW SCORES", 2 }, + { "controls.pause", L"PAUSE MENU", 2 }, +}; + +static struct +{ + /* the bindings shown, until OK writes them */ + char bindings[NUMBEROF(controls)][CONTROL_BINDINGS][CONTROL_NAME_LENGTH]; + /* the binding left and right choose, and the one being set */ + short slot; + short capturing_control; + short capturing_slot; +} controls_screen = { { { { 0 } } }, 0, NONE, 0 }; + +/* a setting's bindings ("Left Ctrl, C") into the control's */ +static void control_bindings_read(short control, char const *text) +{ + short slot; + + for (slot = 0; slot < CONTROL_BINDINGS; slot++) + { + char *binding = controls_screen.bindings[control][slot]; + unsigned int length; + + while (*text == ' ' || *text == ',') + text++; + length = (unsigned int)strcspn(text, ","); + if (length >= CONTROL_NAME_LENGTH) + length = CONTROL_NAME_LENGTH - 1; + memcpy(binding, text, length); + binding[length] = 0; + while (length && binding[length - 1] == ' ') + binding[--length] = 0; + text += strcspn(text, ","); + } +} + +static void control_bindings_text(short control, char *text, unsigned int size) +{ + char const *first = controls_screen.bindings[control][0]; + char const *second = controls_screen.bindings[control][1]; + + snprintf(text, size, "%s%s%s", *first ? first : second, *first && *second ? ", " : "", *first ? second : ""); +} + +/* "controls screen init" (and "controls screen defaults": the defaults) */ +static boolean controls_load(boolean defaults) +{ + short control; + + for (control = 0; control < NUMBEROF(controls); control++) + { + char text[128]; + + if (defaults) + config_default(controls[control].setting, text, sizeof(text)); + else + snprintf(text, sizeof(text), "%s", config_string(controls[control].setting)); + control_bindings_read(control, text); + } + controls_screen.capturing_control = NONE; + return TRUE; +} + +/* "controls screen change set" (OK) */ +static boolean controls_save(void) +{ + short control; + boolean result = TRUE; + + for (control = 0; control < NUMBEROF(controls); control++) + { + char text[128]; + + control_bindings_text(control, text, sizeof(text)); + if (strcmp(text, config_string(controls[control].setting))) + { + if (config_write(controls[control].setting, text)) + platform_log("menus: %s = %s", controls[control].setting, text); + else + result = FALSE; + } + } + return result; +} + +static struct widget_instance *control_row(struct widget_instance *widget) +{ + for (; widget; widget = widget->parent) + { + if (!strncmp(widget->name, "op_command_", 11)) + return widget; + } + return NULL; +} + +/* the control on the row (op_command_) in the group shown, else NONE */ +static short control_of_row(struct widget_instance *row, short group) +{ + short wanted = row ? (short)(atoi(row->name + 11) - 1) : NONE; + short control; + + for (control = 0; control < NUMBEROF(controls) && wanted >= 0; control++) + { + if (controls[control].group == group && wanted-- == 0) + return control; + } + return NONE; +} + +static short controls_group(struct widget_instance *screen) +{ + struct widget_instance *spinner = named(screen, "group_spinner", 0); + + return spinner ? spinner->parameters.list.selected_index : 0; +} + +/* "controls begin binding" (A on a row): the next key or button pressed */ +static boolean control_capture_begin(struct widget_instance *widget) +{ + short control = control_of_row(control_row(widget), controls_group(screen_of(widget))); + + if (control == NONE) + return FALSE; + controls_screen.capturing_control = control; + controls_screen.capturing_slot = controls_screen.slot; + platform_binding_capture_begin(); + return TRUE; +} + +/* the key or button taken (then bound to no other control), or cleared */ +static void control_capture_poll(void) +{ + short control = controls_screen.capturing_control; + short slot = controls_screen.capturing_slot; + int input = -1; + int result; + + if (control == NONE || !(result = platform_binding_capture_poll(&input))) + return; + controls_screen.capturing_control = NONE; + if (result == 1) + { + char name[CONTROL_NAME_LENGTH]; + short other, other_slot; + + halo_input_name(input, name, sizeof(name)); + if (!*name) + return; + for (other = 0; other < NUMBEROF(controls); other++) + { + for (other_slot = 0; other_slot < CONTROL_BINDINGS; other_slot++) + { + if (!_stricmp(controls_screen.bindings[other][other_slot], name)) + controls_screen.bindings[other][other_slot][0] = 0; + } + } + snprintf(controls_screen.bindings[control][slot], CONTROL_NAME_LENGTH, "%s", name); + } + else if (result == 2) + { + controls_screen.bindings[control][slot][0] = 0; + } +} + +/* "controls update menu": the group's rows, their bindings (the one left +and right choose marked on the row chosen), and the help */ +static void controls_update(struct widget_instance *list) +{ + short group = controls_group(list); + struct widget_instance *row; + struct widget_instance *focused_row = control_row(list->focused_child); + + control_capture_poll(); + for (row = list->child; row; row = row->next) + { + short control = control_of_row(row, group); + short slot; + + if (strncmp(row->name, "op_command_", 11)) + continue; + row->visible = control != NONE; + if (control == NONE) + { + if (row == focused_row) + focus_row(list, 0); + continue; + } + text_set(named(row, "command_label", 0), controls[control].label); + for (slot = 0; slot < CONTROL_BINDINGS; slot++) + { + char const *binding = controls_screen.bindings[control][slot]; + wchar_t text[ROW_TEXT_LENGTH]; + char shown[64]; + short index; + + if (controls_screen.capturing_control == control && controls_screen.capturing_slot == slot) + snprintf(shown, sizeof(shown), "%s", "PRESS A KEY"); + else if (row == focused_row && controls_screen.slot == slot) + snprintf(shown, sizeof(shown), "> %s <", *binding ? binding : "-"); + else + snprintf(shown, sizeof(shown), "%s", *binding ? binding : "-"); + for (index = 0; shown[index] && index < ROW_TEXT_LENGTH - 1; index++) + text[index] = (wchar_t)(unsigned char)shown[index]; + text[index] = 0; + text_set(named(row, "command_binding", slot), text); + } + } + if (list->parameters.list.extended_description) + { + list->parameters.list.extended_description->parameters.text_box.string_list_index = + controls_screen.capturing_control != NONE ? 2 : focused_row ? 1 : 0; + } +} + +/* ---------- Multiplayer (port/assets/menus' PLAN: Create Game > Internet +or LAN; Join Game > Server Browser (blank, for now), LAN, Direct Link): the +Xbox's networking, run by the engine's port entry points +(ui_widget_event_handler_functions.c) on our lists */ + +#define MAXIMUM_ADVERTISED_GAMES 9 +#define MULTIPLAYER_MAP_COUNT 13 +#define MAP_ROWS 11 +#define GAMETYPE_ROWS 10 +#define MAXIMUM_GAMETYPES 100 +#define BROWSER_ROWS 15 +#define LOBBY_ROWS 11 +#define TEXT_FIELD_LENGTH 128 +#define PLAYLIST_READ_ONLY_BIT 0x40000000UL +/* (a key stroke's modifier, as input_xbox.c has them: shift, control) */ +#define KEY_MODIFIER_CONTROL_BIT 1 +#define LOBBY_NAME "pc\\main_menu\\multiplayer_type_select\\lobby\\lobby_screen" +#define PREVIEW_NAME "pc\\main_menu\\multiplayer_type_select\\lobby\\preview_screen" +/* the lobby's panel's lines (the gametype, the players, the countdown, the +invite) */ +#define LOBBY_TEXT_LENGTH (ROW_TEXT_LENGTH * 4) + +enum +{ + _multiplayer_mode_server_browser, + _multiplayer_mode_lan, + _multiplayer_mode_direct_link, + _multiplayer_mode_host_internet, + _multiplayer_mode_host_lan, +}; + +enum +{ + _client_state_searching, + _client_state_joining, + _client_state_pregame, + _client_state_ingame, + _client_state_postgame, +}; + +/* a game the client found, as network_client_manager.c has it */ +struct advertised_game +{ + byte key_id[8]; + byte key[16]; + /* XNADDR: size, flags, abEnet (the port's machine identifier), ina */ + byte xnaddr[12]; + byte nonce[8]; + unsigned long update_time; + wchar_t game_name[16]; + long map_version; + char map_name[0x80]; + short engine_type; + word machine_count; + word player_count; + short maximum_player_count; + short unknown100; + short platform; + boolean open; + boolean valid; + boolean has_teams; + boolean oddball_variant; +}; + +typedef char verify_advertised_game_platform_offset[offsetof(struct advertised_game, platform) == 0xDE ? 1 : -1]; +typedef char verify_advertised_game_open_offset[offsetof(struct advertised_game, open) == 0xE0 ? 1 : -1]; + +/* the engine's (port) */ +short ui_widget_port_multiplayer_maps(char const *const **names, short *last_used); +boolean ui_widget_port_multiplayer_map_choose(short level_index); +short ui_widget_port_gametypes(long *indices, short maximum, short *last_used); +boolean ui_widget_port_gametype_choose(long profile_index); +boolean ui_widget_port_host(struct widget_instance *widget, struct event_record *event, boolean *widget_deleted); +boolean ui_widget_port_browse(struct widget_instance *widget, struct event_record *event, boolean *widget_deleted); +boolean ui_widget_port_open(struct widget_instance *widget, char const *name, boolean *widget_deleted); +boolean network_game_client_advertised_game_in_progress(void *client, struct advertised_game *game); +boolean ui_widget_port_join(struct widget_instance *widget, void *advertised_game, char const *lobby_name, + boolean *widget_deleted); +boolean ui_widget_port_multiplayer_player(short controller_index, long profile_index); +void network_game_server_port_set_settings(wchar_t const *name, long maximum_players); +void *global_network_game_client_get(void); +void *global_network_game_server_get(void); +struct advertised_game *network_game_client_get_available_games(void *client); +boolean network_game_client_advertised_game_is_valid(struct advertised_game *game); +short network_game_client_get_state(void *client, short *state_data); +struct network_game *network_game_client_get_game(void *client); +short network_game_client_get_local_machine_index(void); +short network_game_client_get_seconds_to_game_start(void *client); +boolean network_player_is_valid(struct network_player *player); +boolean playlist_profile_get(long index, struct game_variant *variant); +boolean playlist_profile_get_display_name(long index, wchar_t *name); +boolean input_get_key(struct key_stroke *key); +/* the platform layer's */ +int p2p_join_invite(char const *text); +int p2p_invite_link(char *link, int size); +void p2p_set_hosting_allowed(int allowed); +int p2p_peer_address(unsigned char const *identifier, unsigned long *address); +int platform_clipboard_get(char *text, int size); +void platform_clipboard_set(char const *text); +void platform_text_field(int typing); +int config_boolean(char const *name); + +static wchar_t const *const engine_names[] = { L"", L"CTF", L"SLAYER", L"ODDBALL", L"KING OF THE HILL", L"RACE" }; +static short const maximum_players[] = { 2, 4, 8, 12, 16, 24, 32, 48, 64, 96, 128 }; + +static struct +{ + short mode; + /* Create Game's map and gametype lists */ + short map_first, map_chosen; + long gametypes[MAXIMUM_GAMETYPES]; + short gametype_count; + /* (those of the bank the list's spinner shows: STANDARD, CUSTOM) */ + short bank[MAXIMUM_GAMETYPES], bank_count, bank_shown; + short gametype_first, gametype_chosen; + /* the server settings */ + wchar_t game_name[16]; + short maximum_players_index; + /* the browser's games */ + struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES]; + short game_count, game_chosen; + /* the lobby's */ + short lobby_first; + /* the game under way whose lobby is shown before joining it: its key + (the client's slot it is in may come to hold another game) */ + byte preview_key_id[8]; + byte preview_xnaddr[12]; +} multiplayer = { 0, 0, 0, { 0 }, 0, { 0 }, 0, 0, 0, 0, { 0 }, NUMBEROF(maximum_players) - 1 }; + +/* ---- a text field (Direct Link's link, the game's name): the keyboard +types into it (Ctrl+V pastes), its row's A (enter) is done, B (escape) +cancels */ +static struct +{ + struct widget_instance *row; + char text[TEXT_FIELD_LENGTH]; + char before[TEXT_FIELD_LENGTH]; + short maximum; + void (*done)(char const *text); +} text_field; + +/* when the field was last shown (a field not shown for a while is let go +of: its screen has gone) */ +static unsigned long text_field_shown_time; + +static boolean text_field_editing(struct widget_instance *row) +{ + return text_field.row && (!row || text_field.row == row); +} + +static void text_field_begin(struct widget_instance *row, char const *text, short maximum, + void (*done)(char const *text)) +{ + struct key_stroke key; + + text_field.row = row; + snprintf(text_field.text, sizeof(text_field.text), "%s", text); + snprintf(text_field.before, sizeof(text_field.before), "%s", text); + text_field.maximum = (short)MIN(maximum, TEXT_FIELD_LENGTH - 1); + text_field.done = done; + text_field_shown_time = system_milliseconds(); + while (input_get_key(&key)) + ; + platform_text_field(TRUE); +} + +static void text_field_end(boolean keep) +{ + void (*done)(char const *text) = text_field.done; + + platform_text_field(FALSE); + text_field.row = NULL; + text_field.done = NULL; + if (!keep) + snprintf(text_field.text, sizeof(text_field.text), "%s", text_field.before); + else if (done) + done(text_field.text); +} + +static void text_field_insert(char const *text) +{ + size_t length = strlen(text_field.text); + + for (; *text && length < (size_t)text_field.maximum; text++) + { + if (*text >= ' ' && *text <= '~') + text_field.text[length++] = *text; + } + text_field.text[length] = 0; +} + +/* the keys typed since the last frame, and the field shown (a caret +blinking while it is edited) */ + +static void text_field_show(struct widget_instance *value, char const *text, boolean editing) +{ + wchar_t shown[TEXT_FIELD_LENGTH + 2]; + short index; + + if (editing) + { + text_field_shown_time = system_milliseconds(); + struct key_stroke key; + + while (input_get_key(&key)) + { + size_t length = strlen(text_field.text); + + if (key.key_code == _key_backspace) + { + if (length) + text_field.text[length - 1] = 0; + } + else if (TEST_FLAG(key.modifier_flags, KEY_MODIFIER_CONTROL_BIT) && key.key_code == _key_v) + { + char clipboard[TEXT_FIELD_LENGTH]; + + if (platform_clipboard_get(clipboard, sizeof(clipboard))) + text_field_insert(clipboard); + } + else if ((unsigned char)key.ascii_code >= ' ' && (unsigned char)key.ascii_code <= '~') + { + char character[2] = { key.ascii_code, 0 }; + + text_field_insert(character); + } + } + text = text_field.text; + } + for (index = 0; text[index] && index < TEXT_FIELD_LENGTH; index++) + shown[index] = (wchar_t)(unsigned char)text[index]; + if (editing && (system_milliseconds() / 500) % 2) + shown[index++] = L'_'; + shown[index] = 0; + text_set(value, shown); +} + +/* ---- the menu */ + +/* "mp type set mode": the item's (by its name) */ +static void multiplayer_mode_set(struct widget_instance *widget) +{ + char const *name = widget->name; + + /* (an instance's name is cut to 31 characters: + "multiplayer_type_create_interne") */ + multiplayer.mode = + strstr(name, "create_inter") ? _multiplayer_mode_host_internet : + strstr(name, "create_lan") ? _multiplayer_mode_host_lan : + strstr(name, "join_lan") ? _multiplayer_mode_lan : + strstr(name, "join_direct") ? _multiplayer_mode_direct_link : _multiplayer_mode_server_browser; +} + +/* player 1's profile for the controller's player in a network game */ +static boolean multiplayer_player(short controller) +{ + struct player_profile profile; + + if (!campaign_profile(controller, &profile)) + return campaign_fail(); + return ui_widget_port_multiplayer_player(controller, player_ui_get_active_player_profile_index(0)); +} + +/* Create Game's ("join controller to mp game", first): player 1 in, and +the server made: an internet one (an invite, Discord) or a LAN one */ +static boolean multiplayer_host(struct widget_instance *widget, struct event_record *event, short controller, + boolean *widget_deleted) +{ + multiplayer_mode_set(widget); + if (!multiplayer_player(controller)) + return FALSE; + p2p_set_hosting_allowed(multiplayer.mode == _multiplayer_mode_host_internet); + return ui_widget_port_host(widget, event, widget_deleted); +} + +/* ---- the map list */ + +static void map_row_text(short row, wchar_t *text) +{ + string_get("pc\\main_menu\\mp_map_list", (short)(multiplayer.map_first + row), text); +} + +/* "mp level list initialize" */ +static boolean map_list_initialize(struct widget_instance *list) +{ + char const *const *names; + + ui_widget_port_multiplayer_maps(&names, &multiplayer.map_chosen); + multiplayer.map_first = (short)PIN(multiplayer.map_chosen - MAP_ROWS / 2, 0, MULTIPLAYER_MAP_COUNT - MAP_ROWS); + focus_row(list, (short)(multiplayer.map_chosen - multiplayer.map_first)); + return TRUE; +} + +/* "mp map list update" */ +static void map_list_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + struct widget_instance *widget; + short map = list_scroll(list, &multiplayer.map_first, MULTIPLAYER_MAP_COUNT, MAP_ROWS); + + if (map != NONE) + multiplayer.map_chosen = map; + rows_update(list, MAP_ROWS, map_row_text); + if ((widget = named(description, "mp_map_right_name", 0)) != NULL) + widget->parameters.text_box.string_list_index = multiplayer.map_chosen; + if ((widget = named(description, "mp_map_right_pic", 0)) != NULL) + widget->animation.current_frame_index = multiplayer.map_chosen; + if ((widget = named(description, "mp_map_right_data", 0)) != NULL) + widget->parameters.text_box.string_list_index = multiplayer.map_chosen; + profile_name_show(description); +} + +/* (the gametype editor's: Server Setup's copy of the game's gametype) */ +static void gametype_setup_begin(void); +static void gametype_setup_end(void); +static boolean gametype_setup_apply(void); +static void gametype_setup_type(wchar_t *text); + +/* ---- the gametype list: its spinner's bank (STANDARD: the built-in ones, +CUSTOM: those saved) in its first row, then the bank's gametypes */ + +static void gametype_bank_read(short bank) +{ + short index; + + multiplayer.bank_shown = bank; + multiplayer.bank_count = 0; + for (index = 0; index < multiplayer.gametype_count; index++) + { + boolean standard = ((unsigned long)multiplayer.gametypes[index] & PLAYLIST_READ_ONLY_BIT) != 0; + + if (standard == (bank == 0)) + multiplayer.bank[multiplayer.bank_count++] = index; + } + multiplayer.gametype_first = 0; + multiplayer.gametype_chosen = 0; +} + +/* "mp profiles list initialize": on the gametype used last */ +static boolean gametype_list_initialize(struct widget_instance *list) +{ + struct widget_instance *spinner = named(list, "list_item_0_chooser_spinner", 0); + short last, index; + + multiplayer.gametype_count = ui_widget_port_gametypes(multiplayer.gametypes, MAXIMUM_GAMETYPES, &last); + gametype_bank_read(multiplayer.gametype_count && !((unsigned long)multiplayer.gametypes[last] & + PLAYLIST_READ_ONLY_BIT) ? 1 : 0); + for (index = 0; index < multiplayer.bank_count; index++) + { + if (multiplayer.bank[index] == last) + multiplayer.gametype_chosen = index; + } + multiplayer.gametype_first = (short)PIN(multiplayer.gametype_chosen - GAMETYPE_ROWS / 2, 0, + MAX(0, multiplayer.bank_count - GAMETYPE_ROWS)); + if (spinner) + spinner->parameters.list.selected_index = multiplayer.bank_shown; + focus_row(list, (short)(1 + multiplayer.gametype_chosen - multiplayer.gametype_first)); + return TRUE; +} + +/* a gametype's name, in a row's text (the name's own buffer is longer: +playlist_profile_get_display_name fills MAX_GAMENAME characters) */ +static void gametype_display_name(long profile_index, wchar_t *text) +{ + wchar_t name[MAX_GAMENAME]; + + text[0] = 0; + if (playlist_profile_get_display_name(profile_index, name)) + { + ustrncpy(text, name, ROW_TEXT_LENGTH - 1); + text[ROW_TEXT_LENGTH - 1] = 0; + } +} + +static void gametype_name(short item, wchar_t *text) +{ + text[0] = 0; + if (item >= 0 && item < multiplayer.bank_count) + gametype_display_name(multiplayer.gametypes[multiplayer.bank[item]], text); +} + +/* "gt select list update": the bank shown, its rows (from the second), and +the gametype chosen: its name, picture and rules */ +static void gametype_list_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + struct widget_instance *spinner = named(list, "list_item_0_chooser_spinner", 0); + struct widget_instance *row; + struct widget_instance *widget; + short row_index = 0, focused = NONE, count; + + if (spinner && spinner->parameters.list.selected_index != multiplayer.bank_shown) + gametype_bank_read(spinner->parameters.list.selected_index); + count = (short)MIN(multiplayer.bank_count, GAMETYPE_ROWS); + /* (the rows after the spinner's: scrolled as list_scroll does) */ + for (row = list->child; row; row = row->next, row_index++) + { + if (row == list->focused_child && row_index >= 1 && row_index <= GAMETYPE_ROWS) + focused = (short)(row_index - 1); + } + if (focused == count - 1 && multiplayer.gametype_first + count < multiplayer.bank_count) + { + multiplayer.gametype_first++; + focus_row(list, (short)focused); + focused--; + } + else if (focused == 0 && multiplayer.gametype_first > 0) + { + multiplayer.gametype_first--; + focus_row(list, 2); + focused++; + } + if (focused != NONE) + multiplayer.gametype_chosen = (short)(multiplayer.gametype_first + focused); + for (row = list->child, row_index = 0; row; row = row->next, row_index++) + { + wchar_t text[ROW_TEXT_LENGTH]; + + if (row_index < 1 || row_index > GAMETYPE_ROWS) + continue; + row->visible = row_index - 1 < count; + gametype_name((short)(multiplayer.gametype_first + row_index - 1), text); + text_set(named(row, "list_item_text", 0), text); + if (named(row, "list_item_arrows", 0)) + named(row, "list_item_arrows", 0)->visible = row == list->focused_child; + if (named(row, "scroll_up_button", 0)) + named(row, "scroll_up_button", 0)->visible = FALSE; + if (named(row, "scroll_down_button", 0)) + named(row, "scroll_down_button", 0)->visible = FALSE; + } + visible_set(named(description, "locked_gametype_icon", 0), FALSE); + visible_set(named(description, "gametype_right_item", 0), multiplayer.bank_count > 0); + if (multiplayer.gametype_chosen < multiplayer.bank_count) + { + struct game_variant variant; + wchar_t text[ROW_TEXT_LENGTH * 2]; + + gametype_name(multiplayer.gametype_chosen, text); + text_set(named(description, "gametype_right_name", 0), text); + if (playlist_profile_get(multiplayer.gametypes[multiplayer.bank[multiplayer.gametype_chosen]], &variant)) + { + long engine = PIN(variant.game_engine_index, 0, 5); + + if ((widget = named(description, "gametype_right_pic", 0)) != NULL) + widget->animation.current_frame_index = (short)engine; + usnprintf(text, NUMBEROF(text) - 1, L"%s\r\n%s\r\nScore to win: %ld", engine_names[engine], + variant.universal_variant.teams ? L"Teams" : L"Free for all", variant.universal_variant.score_to_win); + text[NUMBEROF(text) - 1] = 0; + text_set(named(description, "gametype_right_data", 0), text); + } + } + profile_name_show(description); +} + +/* "mp profile set for game" */ +static boolean gametype_choose(void) +{ + gametype_setup_end(); + if (multiplayer.gametype_chosen >= multiplayer.bank_count) + return campaign_fail(); + return ui_widget_port_gametype_choose(multiplayer.gametypes[multiplayer.bank[multiplayer.gametype_chosen]]); +} + +/* ---- the server settings */ + +static void game_name_done(char const *text) +{ + short index; + + for (index = 0; text[index] && index < NUMBEROF(multiplayer.game_name) - 1; index++) + multiplayer.game_name[index] = (wchar_t)(unsigned char)text[index]; + multiplayer.game_name[index] = 0; +} + +/* "server settings init": the game's name (player 1's, else the one given +last), the most players, the gametype's copy (once: the screen is made +again on coming back from an option's screen) */ +static boolean server_settings_initialize(struct widget_instance *list) +{ + struct widget_instance *spinner = named(list, "max_players_spinner", 0); + + gametype_setup_begin(); + if (!multiplayer.game_name[0] && player_ui_get_active_player_profile_index(0) != NONE) + { + struct player_profile profile; + + player_ui_get_active_player_profile(0, &profile); + ustrncpy(multiplayer.game_name, profile.player_name, NUMBEROF(multiplayer.game_name) - 1); + } + if (spinner) + spinner->parameters.list.selected_index = multiplayer.maximum_players_index; + return TRUE; +} + +static void wide_to_text(wchar_t const *wide, char *text, short size) +{ + short index; + + for (index = 0; wide[index] && index < size - 1; index++) + text[index] = wide[index] < 0x80 ? (char)wide[index] : '?'; + text[index] = 0; +} + +/* "server settings update" */ +static void server_settings_update(struct widget_instance *list) +{ + struct widget_instance *spinner = named(list, "max_players_spinner", 0); + struct widget_instance *row = named(list, "op_server_name", 0); + char text[TEXT_FIELD_LENGTH]; + + if (spinner) + multiplayer.maximum_players_index = (short)PIN(spinner->parameters.list.selected_index, 0, + NUMBEROF(maximum_players) - 1); + wide_to_text(multiplayer.game_name, text, sizeof(text)); + text_field_show(named(list, "server_name_value", 0), text, text_field_editing(row)); + if (multiplayer.mode != _multiplayer_mode_host_internet) + snprintf(text, sizeof(text), "NONE: A LAN GAME"); + else if (!config_boolean("network.online")) + snprintf(text, sizeof(text), "INTERNET PLAY IS OFF (SETTINGS)"); + else if (!p2p_invite_link(text, sizeof(text))) + snprintf(text, sizeof(text), "MADE WHEN THE GAME STARTS"); + text_field_show(named(list, "invite_value", 0), text, FALSE); + { + wchar_t type[ROW_TEXT_LENGTH]; + + gametype_setup_type(type); + text_set(named(list, "game_type_value", 0), type); + } + settings_help(list); +} + +/* "ss edit server name" (its row's A: begins, or ends) */ +static boolean server_name_edit(struct widget_instance *row) +{ + char text[TEXT_FIELD_LENGTH]; + + if (text_field_editing(row)) + { + text_field_end(TRUE); + return TRUE; + } + wide_to_text(multiplayer.game_name, text, sizeof(text)); + text_field_begin(row, text, NUMBEROF(multiplayer.game_name) - 1, game_name_done); + return TRUE; +} + +/* "ss copy invite" */ +static boolean invite_copy(void) +{ + char link[TEXT_FIELD_LENGTH]; + + if (!p2p_invite_link(link, sizeof(link))) + return campaign_fail(); + platform_clipboard_set(link); + ui_play_audio_feedback_sound(SOUND_FORWARD); + return TRUE; +} + +/* "ss start game": the settings given the server, then the lobby */ +static boolean server_start(void) +{ + if (text_field_editing(NULL)) + text_field_end(TRUE); + network_game_server_port_set_settings(multiplayer.game_name, + maximum_players[PIN(multiplayer.maximum_players_index, 0, NUMBEROF(maximum_players) - 1)]); + /* (the gametype as Server Setup's options left it) */ + if (!gametype_setup_apply()) + return campaign_fail(); + return global_network_game_server_get() != NULL; +} + +/* ---- the browser (Join Game > Server Browser, LAN, Direct Link): the +games found on the LAN, or (Direct Link) from internet play's peers +(reached by an invite link, the clipboard, or Discord) */ + +#define JOIN_GAME_LABELS "pc\\main_menu\\multiplayer_type_select\\join_game\\join_game_ticker_labels" +enum { _join_game_label_servers = 6, _join_game_label_players, _join_game_label_page }; + +static boolean game_from_peer(struct advertised_game const *game) +{ + unsigned long address; + + return p2p_peer_address(game->xnaddr + 2, &address) != 0; +} + +static boolean advertised_in_progress(struct advertised_game *game) +{ + return network_game_client_advertised_game_in_progress(global_network_game_client_get(), game); +} + +static void browser_games_read(void) +{ + void *client = global_network_game_client_get(); + struct advertised_game *games = client ? network_game_client_get_available_games(client) : NULL; + short pass, index; + + multiplayer.game_count = 0; + if (!games || multiplayer.mode == _multiplayer_mode_server_browser) + return; + /* (the open games, then those under way) */ + for (pass = 0; pass < 2; pass++) + { + for (index = 0; index < MAXIMUM_ADVERTISED_GAMES; index++) + { + struct advertised_game *game = &games[index]; + + if (network_game_client_advertised_game_is_valid(game) && !advertised_in_progress(game) == (pass == 0) && + game_from_peer(game) == (multiplayer.mode == _multiplayer_mode_direct_link)) + { + multiplayer.games[multiplayer.game_count++] = game; + } + } + } + if (multiplayer.game_chosen >= multiplayer.game_count) + multiplayer.game_chosen = (short)MAX(0, multiplayer.game_count - 1); +} + +static short browser_row_index(struct widget_instance *row) +{ + return row && !strncmp(row->name, "server_item_", 12) ? (short)(atoi(row->name + 12) - 1) : NONE; +} + +/* a bar of buttons' focus, off one hidden: on its first one shown */ +static void focus_off_hidden(struct widget_instance *bar) +{ + struct widget_instance *button; + short button_index = 0; + + for (button = bar && bar->focused_child && !bar->focused_child->visible ? bar->child : NULL; button; + button = button->next, button_index++) + { + if (button->visible) + { + bar->focused_child = button; + bar->parameters.list.selected_index = button_index; + break; + } + } +} + +/* whether the browser's list's child can take focus (once visible) */ +static boolean browser_item_usable(struct widget_instance *child) +{ + return browser_row_index(child) != NONE || !strcmp(child->name, "join_game_button_bar"); +} + +/* the browser's focus, off what cannot have it (at the start, or a row whose +game went): the first game, else the buttons (off those hidden) */ +static void browser_focus(struct widget_instance *list) +{ + char const *const choices[] = { "server_item_1", "join_game_button_bar" }; + struct widget_instance *focused = list->focused_child; + short index; + + focus_off_hidden(named(list, "join_game_button_bar", 0)); + if (focused && focused->visible && !focused->disabled) + return; + for (index = 0; index < NUMBEROF(choices); index++) + { + struct widget_instance *child; + short child_index = 0; + + for (child = list->child; child; child = child->next, child_index++) + { + if (!strcmp(child->name, choices[index]) && child->visible) + { + list->focused_child = child; + list->parameters.list.selected_index = child_index; + return; + } + } + } +} + +/* "gamespy screen init": the mode's title and parts; the games' client */ +static boolean browser_initialize(struct widget_instance *screen, struct event_record *event, + boolean *widget_deleted) +{ + char const *title = multiplayer.mode == _multiplayer_mode_lan ? "header_lan" : + multiplayer.mode == _multiplayer_mode_direct_link ? "header_direct_link" : "header_server_browser"; + char const *const titles[] = { "header_internet", "header_lan", "header_direct_link", "header_server_browser" }; + char const *const unused[] = { "op_browser_mode", "join_game_button_update", "join_game_button_filters" }; + short index; + + server_browser_initialize(screen); + for (index = 0; index < NUMBEROF(titles); index++) + visible_set(named(screen, titles[index], 0), !strcmp(titles[index], title)); + for (index = 0; index < NUMBEROF(unused); index++) + visible_set(named(screen, unused[index], 0), FALSE); + visible_set(named(screen, "button_clipboard", 0), multiplayer.mode == _multiplayer_mode_direct_link); + { + struct widget_instance *list = named(screen, "join_game_items_list", 0); + struct widget_instance *child; + + /* (only the games' rows and the buttons take focus: not the column titles, which sort nothing, nor the ticker) */ + for (child = list ? list->child : NULL; child; child = child->next) + child->disabled = !browser_item_usable(child); + if (list) + browser_focus(list); + } + visible_set(named(screen, "scroll_up_button", 0), FALSE); + visible_set(named(screen, "scroll_down_button", 0), FALSE); + /* (the columns' sort arrows, both of each over its title: the list is + not sorted) */ + for (index = 0; named(screen, "header_sort_arrows", index); index++) + visible_set(named(screen, "header_sort_arrows", index), FALSE); + multiplayer.game_chosen = 0; + if (multiplayer.mode == _multiplayer_mode_server_browser) + return TRUE; + return ui_widget_port_browse(screen, event, widget_deleted); +} + +static void game_map_name(struct advertised_game const *game, wchar_t *text) +{ + char const *const *names; + short last, index; + short count = ui_widget_port_multiplayer_maps(&names, &last); + + for (index = 0; index < count; index++) + { + if (!_stricmp(names[index], game->map_name)) + { + string_get("pc\\main_menu\\mp_map_list", index, text); + return; + } + } + for (index = 0; game->map_name[index] && index < ROW_TEXT_LENGTH - 1; index++) + text[index] = (wchar_t)(unsigned char)game->map_name[index]; + text[index] = 0; +} + +/* "gamespy screen update": the games' rows (name, map, gametype, players), +the one chosen's line, the counts over the titles (the PC version's: its +players, page, servers) */ +static void browser_update(struct widget_instance *list) +{ + struct widget_instance *row; + struct widget_instance *stats = list->parameters.list.extended_description; + short focused = browser_row_index(list->focused_child); + + browser_games_read(); + if (focused != NONE && focused < multiplayer.game_count) + multiplayer.game_chosen = focused; + for (row = list->child; row; row = row->next) + { + short index = browser_row_index(row); + struct advertised_game *game; + wchar_t text[ROW_TEXT_LENGTH]; + + if (index == NONE) + continue; + row->visible = index < multiplayer.game_count; + if (index >= multiplayer.game_count) + continue; + game = multiplayer.games[index]; + ustrncpy(text, game->game_name, NUMBEROF(game->game_name) - 1); + text[NUMBEROF(game->game_name) - 1] = 0; + text_set(named(row, "server_item_server_name", 0), text); + game_map_name(game, text); + text_set(named(row, "server_item_map", 0), text); + text_set(named(row, "server_item_type", 0), engine_names[PIN(game->engine_type, 0, 5)]); + usnprintf(text, ROW_TEXT_LENGTH - 1, L"%d/%d", game->player_count, game->maximum_player_count); + text_set(named(row, "server_item_players", 0), text); + text_set(named(row, "server_item_ping", 0), advertised_in_progress(game) ? L"LIVE" : L""); + visible_set(named(row, "server_item_locked", 0), FALSE); + visible_set(named(row, "server_item_dedicated", 0), FALSE); + visible_set(named(row, "server_item_classic", 0), FALSE); + } + { + wchar_t text[ROW_TEXT_LENGTH * 2]; + + if (multiplayer.mode == _multiplayer_mode_server_browser) + text[0] = 0; + else if (!multiplayer.game_count) + { + usnprintf(text, NUMBEROF(text) - 1, L"%s", multiplayer.mode == _multiplayer_mode_direct_link ? + L"Copy an invite link and PASTE LINK, or accept a Discord invite" : + L"Looking for games on your LAN..."); + } + else + { + struct advertised_game *game = multiplayer.games[multiplayer.game_chosen]; + + usnprintf(text, NUMBEROF(text) - 1, L"%d players of %d, on %d machines%s", game->player_count, + game->maximum_player_count, game->machine_count, advertised_in_progress(game) ? L": under way" : L""); + } + text[NUMBEROF(text) - 1] = 0; + text_set(named(list, "ticker_player_info", 0), text); + text_set(named(list, "ticker_rules_info", 0), L""); + } + if (stats) + { + wchar_t label[ROW_TEXT_LENGTH], text[ROW_TEXT_LENGTH]; + long players = 0; + short index; + + for (index = 0; index < multiplayer.game_count; index++) + players += multiplayer.games[index]->player_count; + string_get(JOIN_GAME_LABELS, _join_game_label_players, label); + usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s %ld", label, players); + text_set(named(stats, "player_count_label", 0), text); + /* (one page: the list holds every game found) */ + string_get(JOIN_GAME_LABELS, _join_game_label_page, label); + usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s 1/1", label); + text_set(named(stats, "page_count_label", 0), text); + string_get(JOIN_GAME_LABELS, _join_game_label_servers, label); + usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s %d", label, multiplayer.game_count); + text_set(named(stats, "server_count_label", 0), text); + } + browser_focus(list); +} + +/* "direct ip connect go" (Direct Link's PASTE LINK): the invite +link on the clipboard reached, its game then in the list */ +static boolean direct_link_from_clipboard(void) +{ + char text[TEXT_FIELD_LENGTH]; + char *link = text; + size_t length; + + if (!platform_clipboard_get(text, sizeof(text))) + text[0] = 0; + while (*link == ' ' || *link == '\t' || *link == '\r' || *link == '\n') + link++; + for (length = strlen(link); length && (link[length - 1] == ' ' || link[length - 1] == '\t' || + link[length - 1] == '\r' || link[length - 1] == '\n'); length--) + link[length - 1] = 0; + if (!*link || !p2p_join_invite(link)) + { + platform_log("menus: the clipboard has no invite link"); + return campaign_fail(); + } + return TRUE; +} + +/* joining the game chosen: player 1 in, then the lobby */ +static boolean browser_join(struct widget_instance *widget, short controller, boolean *widget_deleted) +{ + if (multiplayer.game_chosen >= multiplayer.game_count) + return campaign_fail(); + if (!multiplayer_player(controller)) + return FALSE; + return ui_widget_port_join(widget, multiplayer.games[multiplayer.game_chosen], LOBBY_NAME, widget_deleted); +} + +/* "gamespy select item" (a row: joins its game) and "gamespy select button" +(Join) */ +static boolean browser_select(struct widget_instance *widget, struct event_record *event, short controller, + boolean *widget_deleted) +{ + short row = browser_row_index(widget); + + if (row != NONE) + multiplayer.game_chosen = row; + if (row != NONE || strstr(widget->name, "button_join")) + { + /* (a game under way: its lobby first, then JOIN GAME) */ + if (multiplayer.game_chosen < multiplayer.game_count && + advertised_in_progress(multiplayer.games[multiplayer.game_chosen])) + { + csmemcpy(multiplayer.preview_key_id, multiplayer.games[multiplayer.game_chosen]->key_id, + sizeof(multiplayer.preview_key_id)); + csmemcpy(multiplayer.preview_xnaddr, multiplayer.games[multiplayer.game_chosen]->xnaddr, + sizeof(multiplayer.preview_xnaddr)); + return ui_widget_port_open(widget, PREVIEW_NAME, widget_deleted); + } + return browser_join(widget, controller, widget_deleted); + } + return TRUE; +} + +/* ---- the lobby: the game's players (up to the port's 128), its map and +gametype, the countdown */ + +static struct network_player *lobby_players[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; +static short lobby_player_count; + +static void lobby_row_text(short row, wchar_t *text) +{ + struct network_player *player = lobby_players[multiplayer.lobby_first + row]; + struct network_game *game = network_game_client_get_game(global_network_game_client_get()); + wchar_t name[NUMBEROF(player->name) + 1]; + + ustrncpy(name, player->name, NUMBEROF(player->name)); + name[NUMBEROF(player->name)] = 0; + if (game && game->variant.universal_variant.teams) + usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s (%s)", name, player->team_index ? L"BLUE" : L"RED"); + else + usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s", name); + text[ROW_TEXT_LENGTH - 1] = 0; +} + +/* the lobby's panel's map: its picture and name */ +static void lobby_map_show(struct widget_instance *description, char const *map_name) +{ + char const *const *names; + short last, count = ui_widget_port_multiplayer_maps(&names, &last), map = 19, index; + struct widget_instance *widget; + + for (index = 0; index < count; index++) + { + if (!_stricmp(names[index], map_name)) + map = index; + } + if ((widget = named(description, "lobby_map_pic", 0)) != NULL) + widget->animation.current_frame_index = map; + if ((widget = named(description, "lobby_map_name", 0)) != NULL) + widget->parameters.text_box.string_list_index = map; +} + +/* "port lobby update" */ +static void lobby_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + void *client = global_network_game_client_get(); + struct network_game *game = client ? network_game_client_get_game(client) : NULL; + short state_data, state = client ? network_game_client_get_state(client, &state_data) : NONE; + wchar_t text[LOBBY_TEXT_LENGTH]; + short index; + + lobby_player_count = 0; + for (index = 0; game && state >= _client_state_pregame && index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; index++) + { + if (network_player_is_valid(&game->players[index])) + lobby_players[lobby_player_count++] = &game->players[index]; + } + list_scroll(list, &multiplayer.lobby_first, lobby_player_count, LOBBY_ROWS); + if (multiplayer.lobby_first > MAX(0, lobby_player_count - LOBBY_ROWS)) + multiplayer.lobby_first = (short)MAX(0, lobby_player_count - LOBBY_ROWS); + rows_update(list, (short)MIN(lobby_player_count, LOBBY_ROWS), lobby_row_text); + visible_set(named(list, "lobby_button_team", 0), game && game->variant.universal_variant.teams); + /* (the buttons' focus, off Switch Team when it is hidden) */ + focus_off_hidden(named(list, "lobby_button_bar", 0)); + visible_set(named(description, "lobby_right_item", 0), game != NULL && state >= _client_state_pregame); + if (!game || state < _client_state_pregame) + { + profile_name_show(description); + return; + } + lobby_map_show(description, game->map.name); + { + short seconds = network_game_client_get_seconds_to_game_start(client); + char link[TEXT_FIELD_LENGTH]; + wchar_t gametype[NUMBEROF(game->variant.human_readable_game_description) + 1]; + + ustrncpy(gametype, game->variant.human_readable_game_description, NUMBEROF(gametype) - 1); + gametype[NUMBEROF(gametype) - 1] = 0; + usnprintf(text, NUMBEROF(text) - 1, L"%s\r\n%s\r\n%d of %d players\r\n\r\n%s", gametype, + engine_names[PIN(game->variant.game_engine_index, 0, 5)], lobby_player_count, game->maximum_players, + seconds > 0 ? L"Starting in:" : game->machine_count < 2 ? L"Waiting for players" : L""); + text[NUMBEROF(text) - 1] = 0; + if (seconds > 0) + { + size_t length = ustrlen(text); + + usnprintf(text + length, NUMBEROF(text) - 1 - length, L" %d", seconds); + } + if (global_network_game_server_get() && p2p_invite_link(link, sizeof(link))) + { + size_t length = ustrlen(text); + + usnprintf(text + length, NUMBEROF(text) - 1 - length, L"\r\n\r\nInvite link copied:\r\npaste it to friends"); + } + text_set_length(named(description, "lobby_game_data", 0), text, LOBBY_TEXT_LENGTH); + } + profile_name_show(description); +} + +/* ---- an in-progress game's lobby, before joining it (Direct Link and +LAN's rows of games under way): what its advertisement tells (no players' +names: they come with joining), JOIN GAME */ + +/* the previewed game, found again among the client's (NULL: gone) */ +static struct advertised_game *preview_game(void) +{ + void *client = global_network_game_client_get(); + struct advertised_game *games = client ? network_game_client_get_available_games(client) : NULL; + short index; + + for (index = 0; games && index < MAXIMUM_ADVERTISED_GAMES; index++) + { + if (network_game_client_advertised_game_is_valid(&games[index]) && + !csmemcmp(games[index].key_id, multiplayer.preview_key_id, sizeof(multiplayer.preview_key_id)) && + !csmemcmp(games[index].xnaddr, multiplayer.preview_xnaddr, sizeof(multiplayer.preview_xnaddr))) + { + return &games[index]; + } + } + return NULL; +} + +/* "port lobby preview update" */ +static void preview_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + struct advertised_game *game = preview_game(); + boolean valid = game != NULL; + wchar_t text[LOBBY_TEXT_LENGTH], name[NUMBEROF(game->game_name) + 1]; + + visible_set(named(description, "lobby_right_item", 0), valid); + if (valid) + { + lobby_map_show(description, game->map_name); + usnprintf(text, NUMBEROF(text) - 1, L"%s\r\n%d of %d players\r\non %d machines", engine_names[PIN(game->engine_type, 0, 5)], + game->player_count, game->maximum_player_count, game->machine_count); + text[NUMBEROF(text) - 1] = 0; + text_set_length(named(description, "lobby_game_data", 0), text, LOBBY_TEXT_LENGTH); + ustrncpy(name, game->game_name, NUMBEROF(game->game_name)); + name[NUMBEROF(game->game_name)] = 0; + /* (the text box does not wrap: lines of up to 28 characters) */ + usnprintf(text, NUMBEROF(text) - 1, L"%s\r\n\r\nThis game is under way.\r\n\r\n%s", name, + game->open ? L"JOIN GAME joins it now;\r\nits players show then." : + L"It cannot be joined now:\r\nit is loading, over or full."); + } + else + usnprintf(text, NUMBEROF(text) - 1, L"The game is gone."); + text[NUMBEROF(text) - 1] = 0; + text_set_length(named(list, "preview_status", 0), text, LOBBY_TEXT_LENGTH); + profile_name_show(description); +} + +/* "port lobby preview join" */ +static boolean preview_join(struct widget_instance *widget, short controller, boolean *widget_deleted) +{ + struct advertised_game *game = preview_game(); + + if (!game || !game->open) + return campaign_fail(); + if (!multiplayer_player(controller)) + return FALSE; + return ui_widget_port_join(widget, game, LOBBY_NAME, widget_deleted); +} + +/* ---- the gametype editor: Edit Gametypes' list (the built-in gametypes +and those saved), the gametype being edited (player_ui's) or Server Setup's +(a copy of the game's gametype), its options' screens: each option a +spinner, found by its name (the PC version's screens put them in another +order than the Xbox's, whose functions of the same names walk them by +place) */ + +boolean ui_widget_port_gametype_edit_begin(long profile_index); +boolean ui_widget_port_gametype_save(struct widget_instance *widget, boolean *widget_deleted); +boolean ui_widget_port_game_variant_set(struct game_variant *variant, struct game_variant_options const *options); +boolean ui_widget_port_gametype_delete(long profile_index); + +#define GAMETYPE_EDIT_ROWS 11 + +static struct +{ + /* the list's */ + long gametypes[MAXIMUM_GAMETYPES]; + short count, first, chosen; + boolean stale; + long deleting; + /* Server Setup's copy of the game's gametype, edited in place of + player_ui's (setup) */ + boolean setup; + struct game_variant setup_variant; + struct game_variant_options setup_options; + /* the vehicle screen's side shown */ + short vehicle_side; +} gametype_edit = { { 0 }, 0, 0, 0, TRUE, NONE, FALSE }; + +static struct game_variant *edit_variant(void) +{ + return gametype_edit.setup ? &gametype_edit.setup_variant : player_ui_get_edit_playlist_profile(); +} + +static struct game_variant_options *edit_options(void) +{ + return gametype_edit.setup ? &gametype_edit.setup_options : player_ui_get_edit_playlist_options(); +} + +enum +{ + _option_long, /* a long of the variant */ + _option_byte, /* a byte of the variant */ + _option_flag, /* a bit of the variant's flags (argument), set by value 1 */ + _option_health, /* the variant's health, tenths */ + _option_short, /* a short of the options */ + _option_option_byte, /* a byte of the options */ + _option_radar /* the options' radar players, and the variant's flag */ +}; + +struct gametype_option +{ + char const *spinner; + short kind; + short offset; + unsigned long argument; + short count; + long values[16]; +}; + +#define VARIANT_FIELD(field) (short)offsetof(struct game_variant, field) +#define OPTIONS_FIELD(field) (short)offsetof(struct game_variant_options, field) +#define TIME_LIMITS { 0, 10, 15, 20, 25, 30, 45 } + +static struct gametype_option const gametype_options[] = +{ + /* player options */ + { "number_of_lives_spinner", _option_long, VARIANT_FIELD(universal_variant.lives), 0, 4, { 0, 1, 3, 5 } }, + { "maximum_health_spinner", _option_health, 0, 0, 6, { 5, 10, 15, 20, 30, 40 } }, + { "shields_spinner", _option_flag, 0, FLAG(_game_variant_no_shields_bit), 2, { 0, 1 } }, + { "respawn_time_spinner", _option_long, VARIANT_FIELD(universal_variant.respawn_time), 0, 4, { 0, 150, 300, 450 } }, + { "respawn_time_growth_spinner", _option_long, VARIANT_FIELD(universal_variant.respawn_time_growth), 0, 4, + { 0, 150, 300, 450 } }, + { "odd_man_out_spinner", _option_byte, VARIANT_FIELD(universal_variant.odd_man_out), 0, 2, { 1, 0 } }, + { "invisible_players_spinner", _option_flag, 0, FLAG(_game_variant_always_invisible_bit), 2, { 1, 0 } }, + { "suicide_penalty_spinner", _option_long, VARIANT_FIELD(universal_variant.suicide_penalty), 0, 4, + { 0, 150, 300, 450 } }, + /* item options (weapon sets: the PC's list, then the Xbox's NO GRENADES) */ + { "item_options_infinite_grenades_spinner", _option_flag, 0, FLAG(_game_variant_infinite_grenades_bit), 2, + { 1, 0 } }, + { "item_options_weapon_set_spinner", _option_long, VARIANT_FIELD(universal_variant.weapon_set), 0, 14, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 10 } }, + { "item_options_starting_equipment_spinner", _option_flag, 0, FLAG(_game_variant_generic_starting_equipment_bit), + 2, { 0, 1 } }, + /* (the loadout: the weapon set's, or each player's two weapons) */ + { "loadout_spinner", _option_option_byte, OPTIONS_FIELD(loadout), 0, 2, { _loadout_category, _loadout_custom } }, + { "primary_weapon_spinner", _option_option_byte, OPTIONS_FIELD(primary_weapon), 0, NUMBER_OF_LOADOUT_WEAPONS, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 } }, + { "secondary_weapon_spinner", _option_option_byte, OPTIONS_FIELD(secondary_weapon), 0, NUMBER_OF_LOADOUT_WEAPONS, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 } }, + /* indicator options */ + { "indicator_options_radar display_spinner", _option_long, VARIANT_FIELD(universal_variant.goal_radar), 0, 3, + { 0, 1, 2 } }, + { "indicator_options_players_on_radar_spinner", _option_radar, OPTIONS_FIELD(radar_players), 0, 3, + { _radar_players_all, _radar_players_friends, _radar_players_none } }, + { "indicator_options_friends_on_screen_spinner", _option_flag, 0, FLAG(_game_variant_allow_friendly_navpoints_bit), + 2, { 1, 0 } }, + /* capture the flag */ + { "assault_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.ctf.assault), 0, 2, { 1, 0 } }, + { "single_flag_spinner", _option_long, VARIANT_FIELD(game_engine_variant.ctf.single_flag_time), 0, 6, + { 0, 1800, 3600, 5400, 9000, 18000 } }, + { "flag_must_reset_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.ctf.flag_must_reset), 0, 2, + { 1, 0 } }, + { "flag_at_home_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.ctf.flag_at_home_to_score), 0, 2, + { 1, 0 } }, + { "captures_to_win_spinner", _option_long, VARIANT_FIELD(universal_variant.score_to_win), 0, 5, + { 1, 3, 5, 10, 15 } }, + { "time_limit_spinner", _option_short, OPTIONS_FIELD(time_limit), 0, 7, TIME_LIMITS }, + /* king of the hill */ + { "koth_moving_hill_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.king.moving_hill), 0, 2, { 1, 0 } }, + { "koth_score_to_win_spinner", _option_long, VARIANT_FIELD(universal_variant.score_to_win), 0, 5, + { 1, 2, 5, 10, 15 } }, + { "koth_team_play_spinner", _option_byte, VARIANT_FIELD(universal_variant.teams), 0, 2, { 1, 0 } }, + /* oddball */ + { "trait_with_ball_spinner", _option_long, VARIANT_FIELD(game_engine_variant.oddball.trait_with_ball), 0, 4, + { 0, 1, 2, 3 } }, + { "trait_without_ball_spinner", _option_long, VARIANT_FIELD(game_engine_variant.oddball.trait_without_ball), 0, 4, + { 0, 1, 2, 3 } }, + { "speed_with_ball_spinner", _option_long, VARIANT_FIELD(game_engine_variant.oddball.speed_with_ball), 0, 3, + { 1, 0, 2 } }, + { "ball_type_spinner", _option_long, VARIANT_FIELD(game_engine_variant.oddball.oddball_ball_type), 0, 3, + { 0, 1, 2 } }, + { "random_start_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.oddball.random_start), 0, 2, { 1, 0 } }, + { "ball_spawn_count_spinner", _option_long, VARIANT_FIELD(game_engine_variant.oddball.ball_spawn_count), 0, 16, + { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } }, + { "score_to_win_spinner", _option_long, VARIANT_FIELD(universal_variant.score_to_win), 0, 5, { 1, 2, 5, 10, 15 } }, + /* (Slayer's, Oddball's and Race's) */ + { "team_play_spinner", _option_byte, VARIANT_FIELD(universal_variant.teams), 0, 2, { 1, 0 } }, + /* race */ + { "team_scoring_spinner", _option_long, VARIANT_FIELD(game_engine_variant.race.team_scoring), 0, 3, { 0, 1, 2 } }, + { "race_type_spinner", _option_long, VARIANT_FIELD(game_engine_variant.race.race_type), 0, 3, { 0, 1, 2 } }, + { "laps_to_win_spinner", _option_long, VARIANT_FIELD(universal_variant.score_to_win), 0, 6, + { 1, 3, 5, 10, 15, 25 } }, + /* slayer (the PC's rows' names and labels are crossed: kill_penalty's + row is labelled KILL IN ORDER, kill_in_order's KILL PENALTY; each sets + what its label says) */ + { "death_bonus_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.slayer.no_death_bonus), 0, 2, { 0, 1 } }, + { "kill_penalty_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.slayer.kill_in_order), 0, 2, { 1, 0 } }, + { "kill_in_order_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.slayer.no_kill_penalty), 0, 2, + { 0, 1 } }, + { "kills_to_win_spinner", _option_long, VARIANT_FIELD(universal_variant.score_to_win), 0, 5, { 5, 10, 15, 25, 50 } }, + /* team options */ + { "friendly_fire_spinner", _option_short, OPTIONS_FIELD(friendly_fire), 0, 4, + { _friendly_fire_off, _friendly_fire_on, _friendly_fire_shields_only, _friendly_fire_explosives_only } }, + { "friendly_fire_penalty_spinner", _option_short, OPTIONS_FIELD(friendly_fire_penalty), 0, 4, { 0, 5, 10, 15 } }, + { "autobalance_spinner", _option_option_byte, OPTIONS_FIELD(auto_team_balance), 0, 2, { 0, 1 } }, + /* vehicle options (the side's set and counts: vehicles_update) */ + { "vehicles_respawn_spinner", _option_short, OPTIONS_FIELD(vehicle_respawn_time), 0, 7, + { 0, 30, 60, 90, 120, 180, 300 } }, +}; + +/* (an instance's name is its definition's cut to 31 characters: +"item_options_infinite_grenades_") */ +#define WIDGET_NAME_LENGTH 31 + +static struct gametype_option const *gametype_option_named(char const *spinner) +{ + short index; + + for (index = 0; index < NUMBEROF(gametype_options); index++) + { + if (!strncmp(gametype_options[index].spinner, spinner, WIDGET_NAME_LENGTH)) + return &gametype_options[index]; + } + return NULL; +} + +static long gametype_option_value(struct gametype_option const *option, struct game_variant *variant, + struct game_variant_options *options) +{ + byte *v = (byte *)variant; + byte *o = (byte *)options; + + switch (option->kind) + { + case _option_long: return *(long *)(v + option->offset); + case _option_byte: return v[option->offset] != 0; + case _option_flag: return (variant->universal_variant.flags & option->argument) != 0; + case _option_health: return (long)(variant->universal_variant.health * 10.0f + 0.5f); + case _option_short: return *(short *)(o + option->offset); + case _option_option_byte: return o[option->offset]; + case _option_radar: return options->radar_players; + } + return 0; +} + +static void gametype_option_value_set(struct gametype_option const *option, long value, struct game_variant *variant, + struct game_variant_options *options) +{ + byte *v = (byte *)variant; + byte *o = (byte *)options; + + switch (option->kind) + { + case _option_long: *(long *)(v + option->offset) = value; break; + case _option_byte: v[option->offset] = (byte)value; break; + case _option_flag: + if (value) + variant->universal_variant.flags |= option->argument; + else + variant->universal_variant.flags &= ~option->argument; + break; + case _option_health: variant->universal_variant.health = (real)value / 10.0f; break; + case _option_short: *(short *)(o + option->offset) = (short)value; break; + case _option_option_byte: o[option->offset] = (byte)value; break; + case _option_radar: + /* (and the Xbox's flag: other players on the tracker or not) */ + options->radar_players = (byte)value; + if (value == _radar_players_none) + variant->universal_variant.flags &= ~FLAG(_game_variant_draw_object_in_motion_sensor_bit); + else + variant->universal_variant.flags |= FLAG(_game_variant_draw_object_in_motion_sensor_bit); + break; + } +} + +/* the value's place in the option's list: its own, else the nearest */ +static short gametype_option_index(struct gametype_option const *option, long value) +{ + short index, best = 0; + long best_distance = 0x7FFFFFFF; + + for (index = 0; index < option->count; index++) + { + long distance = option->values[index] > value ? option->values[index] - value : value - option->values[index]; + + if (distance < best_distance) + { + best_distance = distance; + best = index; + } + } + return best; +} + +/* each of the screen's option rows' spinner, with the option */ +static void gametype_options_each(struct widget_instance *list, boolean save) +{ + struct game_variant *variant = edit_variant(); + struct game_variant_options *options = edit_options(); + struct widget_instance *row; + + if (!variant || !options) + return; + for (row = list->child; row; row = row->next) + { + struct widget_instance *spinner; + + for (spinner = row->child; spinner; spinner = spinner->next) + { + /* (a spinner: its label's name, cut to 31 characters, can be + the same as its own) */ + struct gametype_option const *option = spinner->type == 2 ? gametype_option_named(spinner->name) : NULL; + + if (!option) + continue; + if (save) + { + short index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1); + + gametype_option_value_set(option, option->values[index], variant, options); + } + else + spinner->parameters.list.selected_index = + gametype_option_index(option, gametype_option_value(option, variant, options)); + } + } +} + +/* the options' list of a button of its bar (OK's): the one above it whose +rows are its options */ +static struct widget_instance *gametype_options_list(struct widget_instance *widget) +{ + struct widget_instance *list; + + for (list = widget; list; list = list->parent) + { + struct widget_instance *child; + + for (child = list->child; child; child = child->next) + { + if (!strncmp(child->name, "op_", 3)) + return list; + } + } + return NULL; +} + +/* ---- the vehicle options: a side's (red's, blue's) set and counts, on +the spinners while its side is shown */ + +static char const *const vehicle_spinners[NUMBER_OF_VARIANT_VEHICLES] = +{ + "warthog_spinner", "ghost_spinner", "scorpion_spinner", "rwarthog_spinner", "banshee_spinner", "cgturret_spinner" +}; +/* (the presets' spinner: the vehicle sets 0 to 7, then CUSTOM) */ +#define VEHICLE_PRESET_CUSTOM 8 + +static struct widget_instance *vehicle_spinner(struct widget_instance *list, char const *name) +{ + struct widget_instance *row; + + for (row = list->child; row; row = row->next) + { + struct widget_instance *spinner = named(row, name, 0); + + if (spinner) + return spinner; + } + return NULL; +} + +static void vehicles_show(struct widget_instance *list) +{ + struct game_variant_options *options = edit_options(); + struct widget_instance *spinner; + short side = gametype_edit.vehicle_side, index; + + if (!options) + return; + if ((spinner = vehicle_spinner(list, "team_spinner")) != NULL) + spinner->parameters.list.selected_index = side; + if ((spinner = vehicle_spinner(list, "vehicle_presets_spinner")) != NULL) + spinner->parameters.list.selected_index = options->vehicle_set[side] == VARIANT_VEHICLE_SET_CUSTOM ? + VEHICLE_PRESET_CUSTOM : (short)MIN(options->vehicle_set[side], VEHICLE_PRESET_CUSTOM - 1); + for (index = 0; index < NUMBER_OF_VARIANT_VEHICLES; index++) + { + if ((spinner = vehicle_spinner(list, vehicle_spinners[index])) != NULL) + spinner->parameters.list.selected_index = + (short)MIN(options->vehicle_counts[side][index], MAXIMUM_VARIANT_VEHICLE_COUNT); + } +} + +/* the side shown's spinners kept (and the Xbox's vehicle set: red's, if it +is one of the Xbox's) */ +static void vehicles_keep(struct widget_instance *list) +{ + struct game_variant *variant = edit_variant(); + struct game_variant_options *options = edit_options(); + struct widget_instance *spinner; + short side = gametype_edit.vehicle_side, index; + + if (!options || !variant) + return; + if ((spinner = vehicle_spinner(list, "vehicle_presets_spinner")) != NULL) + options->vehicle_set[side] = spinner->parameters.list.selected_index >= VEHICLE_PRESET_CUSTOM ? + VARIANT_VEHICLE_SET_CUSTOM : (byte)spinner->parameters.list.selected_index; + for (index = 0; index < NUMBER_OF_VARIANT_VEHICLES; index++) + { + if ((spinner = vehicle_spinner(list, vehicle_spinners[index])) != NULL) + options->vehicle_counts[side][index] = + (byte)PIN(spinner->parameters.list.selected_index, 0, MAXIMUM_VARIANT_VEHICLE_COUNT); + } + variant->universal_variant.vehicle_set = options->vehicle_set[0] <= 4 ? options->vehicle_set[0] : 0; +} + +/* "mp prof vehicles update": the side changed (the one left kept, the other +shown); a count changed makes the side's set CUSTOM */ +static void vehicles_update(struct widget_instance *list) +{ + struct game_variant_options *options = edit_options(); + struct widget_instance *team = vehicle_spinner(list, "team_spinner"); + struct widget_instance *preset = vehicle_spinner(list, "vehicle_presets_spinner"); + short index; + + if (!options) + return; + if (team && team->parameters.list.selected_index != gametype_edit.vehicle_side) + { + vehicles_keep(list); + gametype_edit.vehicle_side = (short)PIN(team->parameters.list.selected_index, 0, 1); + vehicles_show(list); + return; + } + for (index = 0; preset && index < NUMBER_OF_VARIANT_VEHICLES; index++) + { + struct widget_instance *spinner = vehicle_spinner(list, vehicle_spinners[index]); + + if (spinner && spinner->parameters.list.selected_index != + options->vehicle_counts[gametype_edit.vehicle_side][index]) + { + options->vehicle_counts[gametype_edit.vehicle_side][index] = (byte)spinner->parameters.list.selected_index; + preset->parameters.list.selected_index = VEHICLE_PRESET_CUSTOM; + } + } +} + +/* "mp profile init X" (the list's creation): its spinners from the gametype */ +static boolean gametype_options_init(struct widget_instance *list) +{ + if (!edit_variant()) + return campaign_fail(); + gametype_options_each(list, FALSE); + if (named(list, "op_team", 0)) + vehicles_show(list); + return TRUE; +} + +/* "mp profile set X" (OK): the gametype from its spinners */ +static boolean gametype_options_save(struct widget_instance *widget) +{ + struct widget_instance *list = gametype_options_list(widget); + + if (!list || !edit_variant()) + return campaign_fail(); + if (named(list, "op_team", 0)) + vehicles_keep(list); + gametype_options_each(list, TRUE); + return TRUE; +} + +/* ---- Server Setup's options: a copy of the game's gametype (the one chosen +before), edited by the editor's screens, the game's on START GAME */ + +static void gametype_setup_begin(void) +{ + if (gametype_edit.setup) + return; + if (!player_ui_game_variant_specified(&gametype_edit.setup_variant)) + return; + gametype_edit.setup_options = *player_ui_get_game_variant_options(); + gametype_edit.setup = TRUE; + gametype_edit.vehicle_side = 0; +} + +static void gametype_setup_end(void) +{ + gametype_edit.setup = FALSE; +} + +static boolean gametype_setup_apply(void) +{ + boolean applied = !gametype_edit.setup || + ui_widget_port_game_variant_set(&gametype_edit.setup_variant, &gametype_edit.setup_options); + + gametype_edit.setup = FALSE; + return applied; +} + +/* the game type row's: the gametype's name and type */ +static void gametype_setup_type(wchar_t *text) +{ + wchar_t name[NUMBEROF(gametype_edit.setup_variant.human_readable_game_description) + 1]; + + text[0] = 0; + if (!gametype_edit.setup) + return; + ustrncpy(name, gametype_edit.setup_variant.human_readable_game_description, NUMBEROF(name) - 1); + name[NUMBEROF(name) - 1] = 0; + usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s (%s%s)", name, + engine_names[PIN(gametype_edit.setup_variant.game_engine_index, 0, 5)], + gametype_edit.setup_variant.universal_variant.teams ? L", TEAMS" : L""); + text[ROW_TEXT_LENGTH - 1] = 0; +} + +/* "port setup edit" (a Server Setup option's row): its screen edits the copy */ +static boolean gametype_setup_edit(void) +{ + gametype_setup_begin(); + return gametype_edit.setup || campaign_fail(); +} + +/* ---- Edit Gametypes' list */ + +static void gametype_edit_read(void) +{ + short last; + + gametype_edit.count = ui_widget_port_gametypes(gametype_edit.gametypes, MAXIMUM_GAMETYPES, &last); + gametype_edit.chosen = (short)MIN(gametype_edit.chosen, MAX(0, gametype_edit.count - 1)); + gametype_edit.stale = FALSE; +} + +/* "mp profiles list initialize" (Edit Gametypes'): on the gametype used last */ +static boolean gametype_edit_list_initialize(struct widget_instance *list) +{ + short last; + + gametype_edit.setup = FALSE; + gametype_edit.count = ui_widget_port_gametypes(gametype_edit.gametypes, MAXIMUM_GAMETYPES, &last); + gametype_edit.chosen = last; + gametype_edit.first = (short)MAX(0, MIN(last - GAMETYPE_EDIT_ROWS / 2, gametype_edit.count - GAMETYPE_EDIT_ROWS)); + gametype_edit.stale = FALSE; + focus_row(list, (short)(gametype_edit.chosen - gametype_edit.first)); + return TRUE; +} + +static void gametype_edit_row_text(short row, wchar_t *text) +{ + gametype_display_name(gametype_edit.gametypes[gametype_edit.first + row], text); +} + +/* "gt edit list update": the rows (scrolled), the gametype chosen's +picture, name and rules */ +static void gametype_edit_list_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + struct widget_instance *widget; + short focused; + + if (gametype_edit.stale) + gametype_edit_read(); + focused = list_scroll(list, &gametype_edit.first, gametype_edit.count, GAMETYPE_EDIT_ROWS); + if (focused != NONE) + gametype_edit.chosen = (short)MIN(focused, gametype_edit.count - 1); + rows_update(list, (short)MIN(gametype_edit.count, GAMETYPE_EDIT_ROWS), gametype_edit_row_text); + visible_set(named(description, "gametype_right_item", 0), gametype_edit.count > 0); + if (gametype_edit.chosen < gametype_edit.count) + { + long profile_index = gametype_edit.gametypes[gametype_edit.chosen]; + struct game_variant variant; + wchar_t text[ROW_TEXT_LENGTH * 2]; + + visible_set(named(description, "locked_gametype_icon", 0), + ((unsigned long)profile_index & PLAYLIST_READ_ONLY_BIT) != 0); + gametype_display_name(profile_index, text); + text_set(named(description, "gametype_right_name", 0), text); + if (playlist_profile_get(profile_index, &variant)) + { + long engine = PIN(variant.game_engine_index, 0, 5); + + if ((widget = named(description, "gametype_right_pic", 0)) != NULL) + widget->animation.current_frame_index = (short)engine; + usnprintf(text, NUMBEROF(text) - 1, L"%s\r\n%s\r\nScore to win: %ld", engine_names[engine], + variant.universal_variant.teams ? L"Teams" : L"Free for all", variant.universal_variant.score_to_win); + text[NUMBEROF(text) - 1] = 0; + text_set(named(description, "gametype_right_data", 0), text); + } + } + profile_name_show(description); +} + +/* "mp profile begin editing" (the list's custom activation: OK, a row's A) */ +static boolean gametype_edit_begin(void) +{ + if (gametype_edit.chosen >= gametype_edit.count) + return campaign_fail(); + gametype_edit.setup = FALSE; + gametype_edit.vehicle_side = 0; + return ui_widget_port_gametype_edit_begin(gametype_edit.gametypes[gametype_edit.chosen]); +} + +/* "request del playlist profile" (X, DELETE): not a built-in one */ +static boolean gametype_delete_request(void) +{ + long profile_index = gametype_edit.chosen < gametype_edit.count ? gametype_edit.gametypes[gametype_edit.chosen] : + NONE; + + if (profile_index == NONE || ((unsigned long)profile_index & PLAYLIST_READ_ONLY_BIT)) + return campaign_fail(); + gametype_edit.deleting = profile_index; + return TRUE; +} + +/* "final del playlist profile" */ +static boolean gametype_delete_final(void) +{ + boolean deleted = gametype_edit.deleting != NONE && ui_widget_port_gametype_delete(gametype_edit.deleting); + + gametype_edit.deleting = NONE; + gametype_edit.stale = TRUE; + return deleted; +} + +/* ---- the gametype's screen and the gametype type's */ + +/* "get edit game settings name": the gametype's name */ +static void gametype_edit_name(struct widget_instance *text_box) +{ + struct game_variant *variant = edit_variant(); + wchar_t name[NUMBEROF(variant->human_readable_game_description) + 1]; + + if (!variant) + return; + ustrncpy(name, variant->human_readable_game_description, NUMBEROF(name) - 1); + name[NUMBEROF(name) - 1] = 0; + text_set(text_box, name); +} + +/* "game settings lists text update": the help of the option with the +focus, for its value (the description's strings are each option's values' +helps in turn), as the Xbox's, which stops the game on the buttons' row */ +static void gametype_option_help(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + struct widget_instance *row; + short index = 0; + + /* (Item Options: the weapon set's row for a category loadout, the two + weapons' for a custom one) */ + if (named(list, "loadout_spinner", 0)) + { + boolean custom = named(list, "loadout_spinner", 0)->parameters.list.selected_index == _loadout_custom; + + visible_set(named(list, "op_weapon_set", 0), !custom); + visible_set(named(list, "op_primary_weapon", 0), custom); + visible_set(named(list, "op_secondary_weapon", 0), custom); + } + /* (the server browser's filters, hidden: their helps are fewer than + their values) */ + if (!description || !list->focused_child || !strncmp(list->name, "filters", 7)) + return; + for (row = list->child; row; row = row->next) + { + struct widget_instance *spinner; + + for (spinner = row->child; spinner && spinner->type != 2 /* spinner */; spinner = spinner->next) + ; + if (row == list->focused_child) + { + if (spinner) + description->parameters.text_box.string_list_index = (short)(index + spinner->parameters.list.selected_index); + return; + } + if (spinner) + index += spinner->parameters.list.number_of_items; + } +} + +static char const *const engine_items[] = +{ + /* (the type list's rows, in order: game_engine_ctf and so on) */ + "gametype_select_ctf_item", "gametype_select_koth_item", "gametype_select_slayer_item", + "gametype_select_oddball_item", "gametype_select_race_item" +}; +static long const engine_of_item[] = { 1, 4, 2, 3, 5 }; + +/* "mp profile init game engine": the gametype's type focused */ +static boolean gametype_engine_init(struct widget_instance *list) +{ + struct game_variant *variant = edit_variant(); + short index; + + if (!variant) + return campaign_fail(); + for (index = 0; index < NUMBEROF(engine_items); index++) + { + if (engine_of_item[index] == variant->game_engine_index) + focus_row(list, index); + } + return TRUE; +} + +/* "mp profile set game engine" (a type's A): another type clears the type's +own rules (as the Xbox's) */ +static boolean gametype_engine_set(struct widget_instance *item) +{ + struct game_variant *variant = edit_variant(); + short index; + + if (!variant) + return campaign_fail(); + for (index = 0; index < NUMBEROF(engine_items); index++) + { + if (!strcmp(item->name, engine_items[index])) + { + if (variant->game_engine_index != engine_of_item[index]) + csmemset(&variant->game_engine_variant, 0, sizeof(variant->game_engine_variant)); + variant->game_engine_index = engine_of_item[index]; + return TRUE; + } + } + return campaign_fail(); +} + +/* "mp edit profile set rule text": the gametype's type (its string in +ui\multiplayer_game_text, as the Xbox's: capture the flag 3, slayer 4, +oddball 5, king of the hill 6, race 7, unknown 8) */ +static void gametype_engine_name(struct widget_instance *text_box) +{ + static short const strings[] = { 8, 3, 4, 5, 6, 7 }; + struct game_variant *variant = edit_variant(); + + if (variant) + text_box->parameters.text_box.string_list_index = strings[PIN(variant->game_engine_index, 0, 5)]; +} + +/* ---------- public code */ + +boolean pc_menu_event_function_invoke( + struct widget_instance *widget, + struct event_record *event, + long function_index, + boolean *widget_deleted) +{ + short controller = event_controller(widget, event); + + switch (function_index) + { + case _pc_menu_function_quit_game: + platform_request_quit(); + return TRUE; + case _pc_menu_function_setting_load: + return setting_load(widget); + case _pc_menu_function_setting_save: + return setting_save(widget); + } + { + char const *name = pc_menu_function_name(function_index); + + if (!name) + return FALSE; + if (!strcmp(name, "main menu quit game")) + { + platform_request_quit(); + } + else if (!strcmp(name, "profile set edit begin")) + { + return pc_menu_profile_edit_begin(); + } + else if (!strcmp(name, "mouse emit accept event")) + { + event_manager_post_button(controller_of(widget), BUTTON_A); + } + else if (!strcmp(name, "mouse emit back event")) + { + event_manager_post_button(controller_of(widget), BUTTON_B); + } + else if (!strcmp(name, "mouse emit x event")) + { + event_manager_post_button(controller_of(widget), BUTTON_X); + } + else if (!strcmp(name, "emit custom activation event")) + { + struct widget_instance *top = widget; + + while (top->parent) + top = top->parent; + boolean deleted; + + ui_widget_port_dispatch_event(top, EVENT_CUSTOM_ACTIVATION, event_controller(widget, event), &deleted); + if (deleted) + *widget_deleted = TRUE; + } + else if (!strcmp(name, "mp type set mode")) + { + lan_mode = strstr(widget->name, "_lan_") != NULL; + multiplayer_mode_set(widget); + /* (Create's: the map list only for a game made, "join controller + to mp game" before; it fails with the game's port in use, by + another copy of the game) */ + if ((multiplayer.mode == _multiplayer_mode_host_internet || + multiplayer.mode == _multiplayer_mode_host_lan) && !global_network_game_server_get()) + { + platform_log("menus: the game could not be made (is its port in use?)"); + return campaign_fail(); + } + } + else if (!strcmp(name, "gamespy screen init")) + { + return browser_initialize(widget, event, widget_deleted); + } + else if (!strcmp(name, "gamespy select item") || !strcmp(name, "gamespy select button")) + { + return browser_select(widget, event, controller, widget_deleted); + } + else if (!strcmp(name, "gamespy back handler") && text_field_editing(NULL)) + { + text_field_end(FALSE); + } + else if (!strcmp(name, "port setup edit")) + { + return gametype_setup_edit(); + } + else if (!strcmp(name, "mp profile save changes")) + { + return ui_widget_port_gametype_save(widget, widget_deleted); + } + else if (!strcmp(name, "mp profile begin editing")) + { + return gametype_edit_begin(); + } + else if (!strcmp(name, "request del playlist profile")) + { + return gametype_delete_request(); + } + else if (!strcmp(name, "final del playlist profile")) + { + return gametype_delete_final(); + } + else if (!strcmp(name, "mp profile init game engine")) + { + return gametype_engine_init(widget); + } + else if (!strcmp(name, "mp profile set game engine")) + { + return gametype_engine_set(widget); + } + else if (!strncmp(name, "mp profile init ", 16) || !strcmp(name, "mp prof init teamplay options") || + !strcmp(name, "mp prof init vehicle options")) + { + return gametype_options_init(widget); + } + else if ((!strncmp(name, "mp profile set ", 15) && strcmp(name, "mp profile set for game")) || + !strcmp(name, "mp prof save teamplay options") || !strcmp(name, "mp prof save vehicle options")) + { + return gametype_options_save(widget); + } + else if (!strcmp(name, "port lobby preview join")) + { + return preview_join(widget, controller, widget_deleted); + } + else if (!strcmp(name, "direct ip connect go")) + { + return direct_link_from_clipboard(); + } + else if (!strcmp(name, "join controller to mp game")) + { + return multiplayer_host(widget, event, controller, widget_deleted); + } + else if (!strcmp(name, "mp level list initialize")) + { + return map_list_initialize(widget); + } + else if (!strcmp(name, "mp level select")) + { + return ui_widget_port_multiplayer_map_choose(multiplayer.map_chosen); + } + else if (!strcmp(name, "mp profiles list initialize")) + { + if (!strcmp(widget->name, "playlist_select_list")) + return gametype_edit_list_initialize(widget); + return gametype_list_initialize(widget); + } + else if (!strcmp(name, "mp profile set for game")) + { + return gametype_choose(); + } + else if (!strcmp(name, "server settings init")) + { + return server_settings_initialize(widget); + } + else if (!strcmp(name, "ss edit server name")) + { + return server_name_edit(widget); + } + else if (!strcmp(name, "ss copy invite")) + { + return invite_copy(); + } + else if (!strcmp(name, "ss start game")) + { + return server_start(); + } + else if (!strcmp(name, "single prev cl item activated")) + { + item_activated(widget, event_controller(widget, event), widget_deleted); + } + else if (!strcmp(name, "port settings save")) + { + settings_each(screen_of(widget), setting_changed_save); + platform_display_apply(); + } + else if (!strcmp(name, "port settings defaults")) + { + settings_each(screen_of(widget), setting_default_show); + } + else if (!strcmp(name, "controls screen init") || !strcmp(name, "controls screen defaults")) + { + return controls_load(!strcmp(name, "controls screen defaults")); + } + else if (!strcmp(name, "controls screen change set")) + { + return controls_save(); + } + else if (!strcmp(name, "controls begin binding")) + { + return control_capture_begin(widget); + } + else if (!strcmp(name, "player profile list initialize")) + { + return profile_list_initialize(widget); + } + else if (!strcmp(name, "profile manager select")) + { + return profile_choose(controller); + } + else if (!strcmp(name, "request del player profile")) + { + return profile_delete_request(); + } + else if (!strcmp(name, "final del player profile")) + { + return profile_delete(); + } + else if (!strcmp(name, "color picker menu initialize")) + { + return color_list_initialize(widget); + } + else if (!strcmp(name, "color picker select color")) + { + return color_choose(); + } + else if (!strcmp(name, "controls binding slot")) + { + controls_screen.slot = (short)!controls_screen.slot; + } + else if (!strcmp(name, "campaign menu init")) + { + return campaign_menu_initialize(controller); + } + else if (!strcmp(name, "campaign menu continue")) + { + return campaign_continue(controller); + } + else if (!strcmp(name, "initialize sp level list solo")) + { + return level_list_initialize(widget, controller); + } + else if (!strcmp(name, "solo level set map")) + { + return level_choose(); + } + else if (!strcmp(name, "difficulty item select")) + { + return difficulty_start(sibling_index(widget), controller); + } + else if (!strcmp(name, "set difficulty")) + { + main_set_difficulty(PIN(difficulty_shown(widget), 0, 3)); + } + else if (!strcmp(name, "load game menu init")) + { + return saved_game_list_initialize(widget, controller); + } + else if (!strcmp(name, "load game menu activated")) + { + return saved_game_continue(controller); + } + else if (!strcmp(name, "load game menu delete request")) + { + return saved_game_delete_request(); + } + else if (!strcmp(name, "load game menu delete finish")) + { + return saved_game_delete(controller); + } + else if (!strcmp(name, "controls back handler") || !strcmp(name, "gamespy back handler") || + !strcmp(name, "gamespy dismiss error") || !strcmp(name, "gamespy dismiss filters")) + { + ui_widget_port_go_back(widget); + *widget_deleted = TRUE; + } + return TRUE; + } +} + +void pc_menu_game_data_function_invoke( + struct widget_instance *widget, + long function) +{ + char const *name = pc_menu_game_data_input_name(function); + + if (!name) + return; + /* (a text field whose screen has gone: let go of) */ + if (text_field.row && system_milliseconds() - text_field_shown_time > 500) + text_field_end(FALSE); + if (!strcmp(name, "solo map list update")) + level_list_update(widget); + else if (!strcmp(name, "mp map list update")) + map_list_update(widget); + else if (!strcmp(name, "gt select list update")) + gametype_list_update(widget); + else if (!strcmp(name, "server settings update")) + server_settings_update(widget); + else if (!strcmp(name, "gamespy screen update")) + browser_update(widget); + else if (!strcmp(name, "gt edit list update")) + gametype_edit_list_update(widget); + else if (!strcmp(name, "get edit game settings name")) + gametype_edit_name(widget); + else if (!strcmp(name, "game settings lists text update")) + gametype_option_help(widget); + else if (!strcmp(name, "mp edit profile set rule text")) + gametype_engine_name(widget); + else if (!strcmp(name, "mp prof vehicles update")) + vehicles_update(widget); + else if (!strcmp(name, "port lobby preview update")) + preview_update(widget); + else if (!strcmp(name, "port lobby update")) + lobby_update(widget); + else if (!strcmp(name, "port settings help")) + settings_help(widget); + else if (!strcmp(name, "controls update menu")) + controls_update(widget); + else if (!strcmp(name, "color picker update")) + color_list_update(widget); + else if (!strcmp(name, "3wide player profile list update")) + profile_list_update(widget); + else if (!strcmp(name, "load game list update")) + saved_game_list_update(widget); +} diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c new file mode 100644 index 00000000..74a4a29e --- /dev/null +++ b/port/linux/game/menu_tags.c @@ -0,0 +1,1473 @@ +/* +MENU_TAGS.C + +The menus written in XML (halo_menus.h, read by port/linux/src/menu_files.c), +built into the tags the game's widgets are made of when ui.map has loaded +(scenario_tags_load), and added to its tag table: a widget definition +('DeLa') for each , a unicode string list ('ustr') for each + and each widget's own text, and a bitmap group ('bitm') for each +, whose textures are drawn from their PNGs (menu_art_texture, +xbox_textures.c). The game's widgets then run them as their own: +main_screen_shell_load opens pc_menus_root_name()'s. + +Each name the files use is looked up here: our widgets (named pc\), +the map's tags (a path, with its backslashes), the game's event handler and +game data functions, and the port's own (menu_functions.c, from +PC_MENU_FUNCTION_BASE), which include the PC version's (most of which do +nothing yet). Its mouse events are kept, though this engine never sends +them (the port's mouse support clicks with A). Anything that does not resolve, or that the widget +code would stop the game for (a spinner's children, a list from strings +with children), is logged with its file and line and nothing is added: the +game keeps its own menus. + +The tag table cannot grow where it is (the tags' names follow it), so it +is copied, with ours after it; every existing tag keeps its index. All of it +is let go in scenario_tags_unload, before the next map's tags load. +*/ + +#include "cseries.h" +#include "bitmaps/bitmap_group.h" +#include "tag_files/tag_groups.h" +#include "text/text_group.h" +#include "rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.h" + +#include "halo_menus.h" + +#include +#include + +/* the platform layer's (port/linux/src) */ +void platform_log(char const *format, ...); +char const *config_string(char const *name); + +/* cache_files.c's (port) */ +void *cache_files_tag_instances(long *count); +void cache_files_set_tag_instances(void *instances, long count); + +/* the game's (port) */ +char *tag_get_name(long tag_index); +char const *ui_widget_event_handler_function_name(long function_index); + +void menu_tags_loaded(char const *map_name); +void menu_tags_unloaded(void); +char const *pc_menus_root_name(void); +char const *pc_menu_function_name(long function_index); +char const *pc_menu_game_data_input_name(long function_index); +boolean pc_menu_tag(long tag_index); + +/* ---------- constants */ + +#define PC_MENU_TAG_PREFIX "pc\\" +#define UI_WIDGET_DEFINITION_TAG 'DeLa' +#define UNICODE_STRING_LIST_TAG 'ustr' +#define FONT_TAG 'font' +#define SOUND_TAG 'snd!' +#define MAXIMUM_STRINGS 64 +#define MAXIMUM_TEXT 2048 +/* the size of a menu bitmap's D3D texture, which only stands for its PNG */ +#define PLACEHOLDER_SIZE 4 + +enum +{ + _bitmap_type_2d = 0, + _bitmap_format_a8r8g8b8 = 11, + _bitmap_has_power_of_two_dimensions_bit = 0, +}; + +enum +{ + _event_handler_close_current_widget_bit = 0, + _event_handler_close_other_widget_bit, + _event_handler_close_all_widgets_bit, + _event_handler_open_widget_bit, + _event_handler_reload_self_bit, + _event_handler_reload_other_widget_bit, + _event_handler_give_focus_to_widget_bit, + _event_handler_run_function_bit, + _event_handler_replace_self_with_widget_bit, + _event_handler_go_back_to_previous_widget_bit, + _event_handler_run_scenario_script_bit, + _event_handler_try_to_branch_on_failure_bit, +}; + +enum +{ + _widget_type_container, + _widget_type_text_box, + _widget_type_spinner_list, + _widget_type_column_list, +}; + +enum +{ + _list_items_generated_from_string_list_tag_bit = 1, +}; + +enum +{ + _child_widget_use_custom_controller_index_bit = 0, +}; + +/* ---------- structures */ + +struct cache_file_tag_instance +{ + long group_tag; + long parent_group_tags[2]; + long tag_index; + char *name; + void *base_address; + unsigned long unused[2]; +}; + +typedef char verify_cache_file_tag_instance_size[ + sizeof(struct cache_file_tag_instance) == 0x20 ? 1 : -1]; + +/* the widget definition, as ui_widget.c has it */ +struct ui_widget_event_handler_reference +{ + long flags; + short event_type; + short function; + struct tag_reference widget_tag; + struct tag_reference sound_effect; + char script[32]; +}; + +struct ui_widget_child_reference +{ + struct tag_reference widget_tag; + char name[32]; + long flags; + short custom_controller_index; + short vertical_offset; + short horizontal_offset; + byte unknown03A[0x50 - 0x3A]; +}; + +struct ui_widget_conditional_reference +{ + struct tag_reference widget_tag; + char name[32]; + long flags; + short custom_controller_index; + byte unknown036[0x50 - 0x36]; +}; + +struct ui_widget_search_and_replace_reference +{ + char search_string[32]; + short replace_function; +}; + +struct ui_widget_game_data_input_reference +{ + short function; + byte unknown002[0x24 - 0x02]; +}; + +struct ui_widget_definition +{ + short type; + short controller_index; + char name[32]; + rectangle2d bounds; + long flags; + long milliseconds_to_auto_close; + long auto_close_fade_time; + struct tag_reference background_bitmap; + struct tag_block game_data_inputs; + struct tag_block event_handlers; + struct tag_block search_and_replace_functions; + byte unknown06C[0xEC - 0x6C]; + struct tag_reference text_label_string_list; + struct tag_reference text_font; + real_argb_color text_color; + short justification; + word text_box_flags; + byte unknown120[0x12E - 0x120]; + short string_list_index; + short horizontal_offset; + short vertical_offset; + byte unknown134[0x150 - 0x134]; + long list_flags; + struct tag_reference list_header_bitmap; + struct tag_reference list_footer_bitmap; + rectangle2d list_header_bounds; + rectangle2d list_footer_bounds; + byte unknown184[0x1A4 - 0x184]; + struct tag_reference extended_description_widget; + byte unknown1B4[0x2D4 - 0x1B4]; + struct tag_block conditional_widgets; + byte unknown2E0[0x3E0 - 0x2E0]; + struct tag_block child_widgets; +}; + +typedef char verify_ui_widget_event_handler_reference_size[ + sizeof(struct ui_widget_event_handler_reference) == 0x48 ? 1 : -1]; +typedef char verify_ui_widget_child_reference_size[ + sizeof(struct ui_widget_child_reference) == 0x50 ? 1 : -1]; +typedef char verify_ui_widget_conditional_reference_size[ + sizeof(struct ui_widget_conditional_reference) == 0x50 ? 1 : -1]; +typedef char verify_ui_widget_search_and_replace_reference_size[ + sizeof(struct ui_widget_search_and_replace_reference) == 0x22 ? 1 : -1]; +typedef char verify_ui_widget_game_data_input_reference_size[ + sizeof(struct ui_widget_game_data_input_reference) == 0x24 ? 1 : -1]; +typedef char verify_ui_widget_definition_text_color_offset[ + offsetof(struct ui_widget_definition, text_color) == 0x10C ? 1 : -1]; +typedef char verify_ui_widget_definition_list_flags_offset[ + offsetof(struct ui_widget_definition, list_flags) == 0x150 ? 1 : -1]; +typedef char verify_ui_widget_definition_conditional_widgets_offset[ + offsetof(struct ui_widget_definition, conditional_widgets) == 0x2D4 ? 1 : -1]; +typedef char verify_ui_widget_definition_size[ + sizeof(struct ui_widget_definition) == 0x3EC ? 1 : -1]; + +/* a spinner bound to a setting (menu_functions.c) */ +struct pc_menu_setting +{ + long definition_index; + char const *setting; + long value_count; + char const *values[MAXIMUM_STRINGS]; + short loaded_index; +}; + +/* ---------- globals */ + +/* the names the files may use, by their values */ +static char const *const event_names[] = +{ + "a", "b", "x", "y", "black", "white", "left_trigger", "right_trigger", + "up", "down", "left", "right", "start", "back", "left_thumb", "right_thumb", + "stick_up", "stick_down", "stick_left", "stick_right", + "right_stick_up", "right_stick_down", "right_stick_left", "right_stick_right", + "created", "deleted", + /* (the PC version's, which this engine never sends) */ + "get_focus", "lose_focus", "left_mouse", "middle_mouse", "right_mouse", "double_click", "custom_activation", + "post_render", +}; + +static char const *const widget_flag_names[] = +{ + "pass_unhandled_to_focused_child", "pause_game", "flash_bitmap", "up_down_tabs_children", + "left_right_tabs_children", "up_down_tabs_items", "left_right_tabs_items", "no_focused_child", + "pass_unhandled_to_all_children", "render_any_controller", "pass_handled_to_all_children", + "main_menu_if_no_history", "tag_controller_index", "nifty_fx", "no_history", + /* (the PC version's, which this engine has no use for) */ + "force_handle_mouse", "no_widescreen_fill", +}; + +static char const *const widget_type_names[] = { "container", "text", "spinner", "column_list" }; +static char const *const list_flag_names[] = { "items_in_code", "items_from_strings", "one_tooltip", "single_preview" }; +static char const *const text_flag_names[] = { "editable", "password", "flashing", "no_focus_test" }; +static char const *const align_names[] = { "left", "right", "center" }; +/* (build_number and pid are the PC version's, which this engine has not) */ +static char const *const replace_names[] = { "none", "controller", "build_number", "pid" }; + +/* the game data functions, as the tags name them +(ui_widget_game_data_input_functions.c) */ +static char const *const game_data_input_names[] = +{ + "NULL", "player settings menu update desc", "unused", "playlist settings menu update desc", + "gametype select menu update desc", "multiplayer type menu update desc", "solo level select update", + "difficulty menu update desc", "build number textbox only", "server list update", + "network pregame status update", "splitscreen pregame status update", "net splitscreen prejoin players", + "mp profile list update", "3wide player profile list update", "plyr prof edit select menu upd8", + "player profile small menu update", "game settings lists text update", "solo game objective text", + "color picker update", "game settings lists pic update", "main menu fake animate", + "mp level select update", "get active plyr profile name", "get edit plyr profile name", + "get edit game settings name", "get active plyr profile color", "mp set textbox map name", + "mp set textbox game ruleset", "mp set textbox teams noteams", "mp set textbox score limit", + "mp set textbox score limit type", "mp set bitmap for map", "mp set bitmap for ruleset", + "mp set textbox", "mp edit profile set rule text", "system link status check", "mp game directions", + "teams no teams bitmap update", "warn if diff will nuke saved game", "dim if no net cable", +}; + +/* the port's event handler functions (menu_functions.c), from +PC_MENU_FUNCTION_BASE: its own, then the PC version's that the Xbox's have +not (as its tags name them), then its own of the Xbox's names, which our +widgets run instead of the Xbox's (those take the Xbox's widgets: a spinner +of levels, the difficulty list itself) */ +static char const *const port_function_names[] = +{ + "port quit game", "port setting load", "port setting save", "unwired", + "exit gracefully to xbox dashboard", "pause game invert pitch", "start new coop game", + "pause game invert spinner get", "pause game invert spinner set", "main menu quit game", + "mouse emit accept event", "mouse emit back event", "mouse emit dpad left event", + "mouse emit dpad right event", "mouse spinner 3wide click", "controls screen init", "video screen init", + "controls begin binding", "gamespy screen init", "gamespy screen dispose", "gamespy select header", + "gamespy select item", "gamespy select button", "plr prof init mouse set", "plr prof change mouse set", + "plr prof init audio set", "plr prof change audio set", "plr prof change video set", + "controls screen dispose", "controls screen change set", "mouse emit x event", "gamepad screen init", + "gamepad screen dispose", "gamepad screen change gamepads", "gamepad screen select item", + "mouse screen defaults", "audio screen defaults", "video screen defaults", "controls screen defaults", + "profile set edit begin", "profile manager delete", "profile manager select", "gamespy dismiss error", + "server settings init", "ss edit server name", "ss edit server password", "ss start game", + "video test dialog init", "video test dialog dispose", "video test dialog accept", + "gamespy dismiss filters", "gamespy update filter settings", "gamespy back handler", + "mouse spinner 1wide click", "controls back handler", "controls advanced launch", "controls advanced ok", + "mp pause menu open", "mp game options open", "mp choose team", "mp prof init vehicle options", + "mp prof save vehicle options", "single prev cl item activated", "mp prof init teamplay options", + "mp prof save teamplay options", "mp game options choose", "emit custom activation event", + "plr prof cancel audio set", "plr prof init network options", "plr prof save network options", + "difficulty item select", + "gamespy get patch", "video screen dispose", "campaign menu init", "campaign menu continue", + "load game menu init", "load game menu dispose", "load game menu activated", "solo menu save checkpoint", + "mp type set mode", "direct ip connect init", + "direct ip connect go", "direct ip edit field", "network settings edit a port", "network settings defaults", + "load game menu delete request", "load game menu delete finish", + "initialize sp level list solo", "dispose sp level list", "solo level set map", "set difficulty", + "port settings save", "port settings defaults", "controls binding slot", + "color picker menu initialize", "color picker menu dispose", "color picker select color", + "player profile list initialize", "player profile list dispose", "request del player profile", + "final del player profile", + "ss copy invite", "join controller to mp game", "mp level list initialize", "mp level list dispose", + "mp level select", "mp profiles list initialize", "mp profiles list dispose", "mp profile set for game", + "port lobby preview join", + "port setup edit", + /* (the gametype editor's: the Xbox's walk their rows by place, which the + PC version's screens changed) */ + "mp profile begin editing", "mp profile save changes", "request del playlist profile", "final del playlist profile", + "mp profile init game engine", "mp profile set game engine", "mp profile init ctf rules", + "mp profile init koth rules", "mp profile init slayer rules", "mp profile init oddball rules", + "mp profile init racing rules", "mp profile init player opts", "mp profile init item options", + "mp profile init indicator opts", "mp profile set ctf rules", "mp profile set koth rules", + "mp profile set slayer rules", "mp profile set oddball rules", "mp profile set racing rules", + "mp profile set player options", "mp profile set item options", "mp profile set indicator opts", +}; + +/* the PC version's game data functions that the Xbox's have not, from +PC_MENU_FUNCTION_BASE (menu_functions.c) */ +static char const *const port_game_data_input_names[] = +{ + "unwired", + "pause game set textbox inverted", "dim unless two controllers", "controls update menu", + "video menu update", "gamespy screen update", "common button bar update", "gamepad update menu", + "server settings update", "audio menu update", "mp prof vehicles update", "solo map list update", + "mp map list update", "gt select list update", "gt edit list update", "load game list update", + "direct ip connect update", "network settings update", + "port settings help", "color picker update", "3wide player profile list update", "port lobby update", + "port lobby preview update", + /* (the gametype editor's: the Xbox's stops the game on the buttons' + row; the Xbox's read only player_ui's gametype, not Server Setup's) */ + "game settings lists text update", "get edit game settings name", "mp edit profile set rule text", +}; + +static struct +{ + /* what is let go when the map unloads */ + void **blocks; + long block_count; + struct bitmap_data **bitmaps; + long bitmap_count; + struct cache_file_tag_instance *original_instances; + long original_count; + struct pc_menu_setting *settings; + long setting_count; + boolean loaded; + char root[300]; +} menu_tags; + +/* the build under way */ +static struct +{ + struct halo_menus const *menus; + long first_index; + long first_salt; + long next; + /* each widget's, bitmap's and string list's tag index, and each + widget's own text's and strings' (NONE: none) */ + long *widget_tags; + long *bitmap_tags; + long *strings_tags; + long *text_tags; + long *spinner_tags; + boolean failed; +} build; + +/* ---------- private code */ + +/* (the game's allocator, which malloc and free are here: cseries.h) */ +static void *allocate(long size) +{ + void *block = malloc(size > 0 ? size : 1); + + if (!block) + { + build.failed = TRUE; + return NULL; + } + memset(block, 0, size > 0 ? size : 1); + menu_tags.blocks = realloc(menu_tags.blocks, (menu_tags.block_count + 1) * sizeof(*menu_tags.blocks)); + menu_tags.blocks[menu_tags.block_count++] = block; + return block; +} + +static void problem(char const *file, long line, char const *message, char const *detail) +{ + if (!build.failed) + halo_menus_log(file, line, message, detail); + build.failed = TRUE; +} + +static long name_index(char const *name, char const *const *names, long count) +{ + long index; + + for (index = 0; name && index < count; index++) + { + if (!strcmp(name, names[index])) + return index; + } + return NONE; +} + +/* the bits of the space-separated names */ +static long flags_parse(char const *text, char const *const *names, long count, char const *file, long line) +{ + long flags = 0; + + while (text && *text) + { + char word[64]; + long length, bit; + + text += strspn(text, " \t\r\n"); + length = (long)strcspn(text, " \t\r\n"); + if (!length) + break; + if (length >= (long)sizeof(word)) + length = sizeof(word) - 1; + memcpy(word, text, length); + word[length] = 0; + text += strcspn(text, " \t\r\n"); + bit = name_index(word, names, count); + if (bit == NONE) + problem(file, line, "there is no flag", word); + else + flags |= 1L << bit; + } + return flags; +} + +static void reference_clear(struct tag_reference *reference, long group_tag) +{ + reference->group_tag = group_tag; + reference->name = ""; + reference->name_length = 0; + reference->index = NONE; +} + +static void reference_set(struct tag_reference *reference, long group_tag, long tag_index) +{ + if (tag_index == NONE) + { + reference_clear(reference, group_tag); + return; + } + reference->group_tag = group_tag; + reference->name = (char *)tag_get_name(tag_index); + reference->name_length = (long)strlen(reference->name); + reference->index = tag_index; +} + +static long find(char const *name, long count, char const *(*name_of)(long)) +{ + long index; + + for (index = 0; name && index < count; index++) + { + if (!strcmp(name_of(index), name)) + return index; + } + return NONE; +} + +static char const *widget_name(long index) { return build.menus->widgets[index].name; } +static char const *bitmap_name(long index) { return build.menus->bitmaps[index].name; } +static char const *strings_name(long index) { return build.menus->string_lists[index].name; } + +static long widget_named(char const *name) +{ + return find(name, build.menus->widget_count, widget_name); +} + +static long map_tag(long group_tag, char const *name, char const *file, long line) +{ + long index = tag_loaded(group_tag, name); + + if (index == NONE) + problem(file, line, "the map has no tag", name); + return index; +} + +/* a tag: the map's by its path (with a backslash), else one of ours */ +static long tag_named(long group_tag, char const *name, char const *file, long line) +{ + long index = NONE; + + if (strchr(name, '\\')) + return map_tag(group_tag, name, file, line); + switch (group_tag) + { + case UI_WIDGET_DEFINITION_TAG: + index = widget_named(name); + index = index == NONE ? NONE : build.widget_tags[index]; + break; + case BITMAP_GROUP_TAG: + index = find(name, build.menus->bitmap_count, bitmap_name); + index = index == NONE ? NONE : build.bitmap_tags[index]; + break; + case UNICODE_STRING_LIST_TAG: + index = find(name, build.menus->string_list_count, strings_name); + index = index == NONE ? NONE : build.strings_tags[index]; + break; + } + if (index == NONE) + problem(file, line, "there is nothing named", name); + return index; +} + +/* "unwired ": a function the PC version has for these widgets, not +yet written for this engine (port/assets/menus/UNWIRED.md), which does +nothing */ +static boolean unwired(char const *name) +{ + return !strncmp(name, "unwired ", 8); +} + +static long function_index(char const *name, char const *file, long line) +{ + char const *function_name; + long index; + + if (unwired(name)) + name = "unwired"; + index = name_index(name, port_function_names, NUMBEROF(port_function_names)); + if (index != NONE) + return PC_MENU_FUNCTION_BASE + index; + for (index = 0; (function_name = ui_widget_event_handler_function_name(index)) != NULL; index++) + { + if (!strcmp(name, function_name)) + return index; + } + problem(file, line, "there is no event handler function", name); + return NONE; +} + +static long game_data_input_index(char const *name, char const *file, long line) +{ + long index; + + if (unwired(name)) + name = "unwired"; + /* (ours first, as for the event handlers) */ + index = name_index(name, port_game_data_input_names, NUMBEROF(port_game_data_input_names)); + if (index != NONE) + return PC_MENU_FUNCTION_BASE + index; + index = name_index(name, game_data_input_names, NUMBEROF(game_data_input_names)); + if (index != NONE) + return index; + problem(file, line, "there is no game data input", name); + return NONE; +} + +static long hex_digit(char character) +{ + if (character >= '0' && character <= '9') + return character - '0'; + if (character >= 'a' && character <= 'f') + return character - 'a' + 10; + if (character >= 'A' && character <= 'F') + return character - 'A' + 10; + return NONE; +} + +/* "#RRGGBB" or "#AARRGGBB" */ +static boolean parse_color(char const *text, real_argb_color *color) +{ + long length = text ? (long)strlen(text) : 0; + long components[4] = { 255, 0, 0, 0 }; + long index; + + if ((length != 7 && length != 9) || text[0] != '#') + return FALSE; + for (index = 0; index < (length - 1) / 2; index++) + { + long high = hex_digit(text[1 + 2 * index]), low = hex_digit(text[2 + 2 * index]); + + if (high == NONE || low == NONE) + return FALSE; + components[length == 7 ? index + 1 : index] = high * 16 + low; + } + color->alpha = components[0] / 255.0f; + color->red = components[1] / 255.0f; + color->green = components[2] / 255.0f; + color->blue = components[3] / 255.0f; + return TRUE; +} + +/* "top left bottom right" */ +static void parse_bounds(char const *text, rectangle2d *bounds, char const *file, long line) +{ + long top, left, bottom, right; + char extra; + + if (text && sscanf(text, "%ld %ld %ld %ld %c", &top, &left, &bottom, &right, &extra) == 4) + { + bounds->y0 = (short)top; + bounds->x0 = (short)left; + bounds->y1 = (short)bottom; + bounds->x1 = (short)right; + } + else if (text) + { + problem(file, line, "bounds are \"top left bottom right\":", text); + } +} + +/* a 'ustr' of the strings */ +static void *string_list_build(char const *const *strings, long count) +{ + struct string_list *list = allocate(sizeof(struct string_list)); + struct string_list_entry *entries = allocate(count * sizeof(struct string_list_entry)); + long index; + + if (!list || !entries) + return list; + list->strings.count = count; + list->strings.address = entries; + for (index = 0; index < count; index++) + { + long length = (long)strlen(strings[index]); + unsigned short *characters = allocate((length * 2 + 2) * sizeof(unsigned short)); + long written; + + if (!characters) + return list; + written = halo_menus_utf16(strings[index], characters, length * 2 + 2); + entries[index].string.size = written * (long)sizeof(unsigned short); + entries[index].string.address = characters; + } + return list; +} + +/* the "|"-separated pieces of text, copied (at most MAXIMUM_STRINGS) */ +static long split(char const *text, char const **pieces) +{ + long count = 0; + + while (text && count < MAXIMUM_STRINGS) + { + long length = (long)strcspn(text, "|"); + char *piece = allocate(length + 1); + + if (!piece) + break; + memcpy(piece, text, length); + pieces[count++] = piece; + if (!text[length]) + break; + text += length + 1; + } + return count; +} + +static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index) +{ + struct bitmap_group *group = allocate(sizeof(struct bitmap_group)); + struct bitmap_group_sequence *sequence = allocate(sizeof(struct bitmap_group_sequence)); + struct bitmap_data *bitmaps = allocate(source->frame_count * sizeof(struct bitmap_data)); + long frame; + + if (build.failed) + return group; + if (!source->frame_count) + { + problem(source->file, source->line, "a bitmap has no frames:", source->name); + return group; + } + sequence->first_bitmap_index = 0; + sequence->bitmap_count = (short)source->frame_count; + group->sequences.count = 1; + group->sequences.address = sequence; + group->bitmaps.count = source->frame_count; + group->bitmaps.address = bitmaps; + for (frame = 0; frame < source->frame_count; frame++) + { + struct halo_menu_frame const *data = &build.menus->frames[source->first_frame + frame]; + struct bitmap_data *bitmap = &bitmaps[frame]; + + /* (a frame of the map's: a copy of its bitmap, which the texture cache + loads from the map as its own) */ + if (data->map) + { + long group_index = map_tag(BITMAP_GROUP_TAG, data->map, source->file, source->line); + struct bitmap_group *group_source; + + if (group_index == NONE) + return group; + group_source = bitmap_group_get(group_index); + if (data->index < 0 || data->index >= group_source->bitmaps.count) + { + problem(source->file, source->line, "the map's bitmap has no such frame:", data->map); + return group; + } + memcpy(bitmap, (struct bitmap_data *)group_source->bitmaps.address + data->index, sizeof(*bitmap)); + bitmap->cache_block_index = NONE; + bitmap->base_address = NULL; + continue; + } + + if (data->width <= 0 || data->height <= 0 || data->width > 2048 || data->height > 2048) + { + problem(source->file, source->line, "a frame is 1 to 2048 wide and high:", data->png); + return group; + } + /* (its D3D texture is a small one, which its PNG is drawn for: the + game draws a bitmap in units of its size, to whatever texture it has) */ + bitmap->signature = BITMAP_GROUP_TAG; + bitmap->width = PLACEHOLDER_SIZE; + bitmap->height = PLACEHOLDER_SIZE; + bitmap->depth = 1; + bitmap->type = _bitmap_type_2d; + bitmap->format = _bitmap_format_a8r8g8b8; + bitmap->flags = FLAG(_bitmap_has_power_of_two_dimensions_bit); + bitmap->tag_index = tag_index; + bitmap->cache_block_index = NONE; + if (!rasterizer_bitmap_new(bitmap)) + { + problem(source->file, source->line, "could not make the texture of bitmap", source->name); + return group; + } + bitmap->width = (short)data->width; + bitmap->height = (short)data->height; + menu_tags.bitmaps = realloc(menu_tags.bitmaps, (menu_tags.bitmap_count + 1) * sizeof(*menu_tags.bitmaps)); + menu_tags.bitmaps[menu_tags.bitmap_count++] = bitmap; + /* (none without a renderer: debug.null_renderer) */ + if (bitmap->hardware_format) + halo_menus_art_register(bitmap->hardware_format, data->png); + } + return group; +} + +/* adds a conditional widget to the definition, if it has not got it */ +static void conditional_add(struct ui_widget_definition *definition, long tag_index, long flags) +{ + struct ui_widget_conditional_reference *conditionals = + (struct ui_widget_conditional_reference *)definition->conditional_widgets.address; + struct ui_widget_conditional_reference *grown; + long count = definition->conditional_widgets.count, index; + + for (index = 0; index < count; index++) + { + if (conditionals[index].widget_tag.index == tag_index) + return; + } + grown = allocate((count + 1) * sizeof(*grown)); + if (!grown) + return; + /* (the game's memcpy asserts on a NULL source, even for nothing) */ + if (count) + memcpy(grown, conditionals, count * sizeof(*grown)); + reference_set(&grown[count].widget_tag, UI_WIDGET_DEFINITION_TAG, tag_index); + grown[count].flags = flags; + definition->conditional_widgets.address = grown; + definition->conditional_widgets.count = count + 1; +} + +static void handler_build(struct ui_widget_event_handler_reference *handler, struct halo_menu_handler const *source, + long event_type, struct ui_widget_definition *definition) +{ + char const *targets[5]; + long target_count = 0; + + handler->event_type = (short)event_type; + reference_clear(&handler->widget_tag, UI_WIDGET_DEFINITION_TAG); + reference_clear(&handler->sound_effect, SOUND_TAG); + if (source->run) + { + handler->function = (short)function_index(source->run, source->file, source->line); + SET_FLAG(handler->flags, _event_handler_run_function_bit, TRUE); + } + if (source->script) + { + strncpy(handler->script, source->script, sizeof(handler->script) - 1); + SET_FLAG(handler->flags, _event_handler_run_scenario_script_bit, TRUE); + } + else if (source->label) + { + strncpy(handler->script, source->label, sizeof(handler->script) - 1); + } + if (source->open) + { + targets[target_count++] = source->open; + SET_FLAG(handler->flags, _event_handler_open_widget_bit, TRUE); + } + if (source->replace) + { + targets[target_count++] = source->replace; + SET_FLAG(handler->flags, _event_handler_replace_self_with_widget_bit, TRUE); + } + if (source->focus) + { + targets[target_count++] = source->focus; + SET_FLAG(handler->flags, _event_handler_give_focus_to_widget_bit, TRUE); + } + if (source->back) + SET_FLAG(handler->flags, _event_handler_go_back_to_previous_widget_bit, TRUE); + if (source->branch) + SET_FLAG(handler->flags, _event_handler_try_to_branch_on_failure_bit, TRUE); + if (source->close) + { + if (!strcmp(source->close, "current")) + SET_FLAG(handler->flags, _event_handler_close_current_widget_bit, TRUE); + else if (!strcmp(source->close, "all")) + SET_FLAG(handler->flags, _event_handler_close_all_widgets_bit, TRUE); + else if (!strcmp(source->close, "other") && source->widget) + SET_FLAG(handler->flags, _event_handler_close_other_widget_bit, TRUE); + else + problem(source->file, source->line, "close is \"current\", \"all\" or \"other\" (with widget=)", + source->close); + } + if (source->reload) + { + if (!strcmp(source->reload, "self")) + SET_FLAG(handler->flags, _event_handler_reload_self_bit, TRUE); + else if (!strcmp(source->reload, "other") && source->widget) + SET_FLAG(handler->flags, _event_handler_reload_other_widget_bit, TRUE); + else + problem(source->file, source->line, "reload is \"self\" or \"other\" (with widget=)", source->reload); + } + if (source->widget) + targets[target_count++] = source->widget; + if (target_count > 1) + { + long index; + + /* (several flags may share the one widget) */ + for (index = 1; index < target_count; index++) + { + if (strcmp(targets[index], targets[0])) + problem(source->file, source->line, "a handler names one widget (open, replace, focus or widget)", + NULL); + } + } + if (target_count) + { + reference_set(&handler->widget_tag, UI_WIDGET_DEFINITION_TAG, + tag_named(UI_WIDGET_DEFINITION_TAG, targets[0], source->file, source->line)); + } + if (source->sound) + reference_set(&handler->sound_effect, SOUND_TAG, map_tag(SOUND_TAG, source->sound, source->file, source->line)); + if (source->otherwise) + { + if (!source->run) + problem(source->file, source->line, "otherwise needs run (the function whose failure it is for)", NULL); + SET_FLAG(handler->flags, _event_handler_try_to_branch_on_failure_bit, TRUE); + conditional_add(definition, tag_named(UI_WIDGET_DEFINITION_TAG, source->otherwise, source->file, source->line), 1); + } +} + +static void setting_add(struct halo_menu_widget const *source, long definition_index) +{ + struct pc_menu_setting *setting; + + menu_tags.settings = realloc(menu_tags.settings, (menu_tags.setting_count + 1) * sizeof(*menu_tags.settings)); + setting = &menu_tags.settings[menu_tags.setting_count++]; + memset(setting, 0, sizeof(*setting)); + setting->definition_index = definition_index; + setting->setting = source->setting; + setting->loaded_index = NONE; + setting->value_count = split(source->values, setting->values); +} + +/* a font: large, small, terminal, or the map's by its path */ +static long font_tag(char const *font, char const *file, long line) +{ + char const *path = !strcmp(font, "large") ? "ui\\large_ui" : + !strcmp(font, "small") ? "ui\\small_ui" : + !strcmp(font, "terminal") ? "ui\\interstate" : font; + + return map_tag(FONT_TAG, path, file, line); +} + +static void *widget_build(long widget_index) +{ + struct halo_menus const *menus = build.menus; + struct halo_menu_widget const *source = &menus->widgets[widget_index]; + struct ui_widget_definition *definition = allocate(sizeof(struct ui_widget_definition)); + char const *leaf = strrchr(source->name, '/') ? strrchr(source->name, '/') + 1 : source->name; + long child, handler, input, conditional, replace, count; + + if (!definition) + return NULL; + strncpy(definition->name, leaf, sizeof(definition->name) - 1); + definition->type = _widget_type_container; + if (source->type) + { + long type = name_index(source->type, widget_type_names, NUMBEROF(widget_type_names)); + + if (type == NONE) + problem(source->file, source->line, "there is no widget type", source->type); + else + definition->type = (short)type; + } + definition->controller_index = 4; + if (source->controller && strcmp(source->controller, "any")) + { + if (strlen(source->controller) != 1 || source->controller[0] < '1' || source->controller[0] > '4') + problem(source->file, source->line, "controller is 1 to 4, or \"any\":", source->controller); + else + definition->controller_index = (short)(source->controller[0] - '1'); + } + definition->bounds.y0 = (short)source->top; + definition->bounds.x0 = (short)source->left; + definition->bounds.y1 = (short)(source->top + (source->has_height ? source->height : 480)); + definition->bounds.x1 = (short)(source->left + (source->has_width ? source->width : 640)); + definition->flags = flags_parse(source->flags, widget_flag_names, NUMBEROF(widget_flag_names), + source->file, source->line); + definition->milliseconds_to_auto_close = source->auto_close; + definition->auto_close_fade_time = source->auto_close_fade; + reference_clear(&definition->background_bitmap, BITMAP_GROUP_TAG); + if (source->bitmap) + { + reference_set(&definition->background_bitmap, BITMAP_GROUP_TAG, + tag_named(BITMAP_GROUP_TAG, source->bitmap, source->file, source->line)); + } + /* the game data inputs */ + for (count = 0, input = source->first_input; input != HALO_MENU_NONE; input = menus->inputs[input].next) + count++; + if (count) + { + struct ui_widget_game_data_input_reference *inputs = allocate(count * sizeof(*inputs)); + + definition->game_data_inputs.count = count; + definition->game_data_inputs.address = inputs; + for (count = 0, input = source->first_input; inputs && input != HALO_MENU_NONE; input = menus->inputs[input].next) + { + struct halo_menu_input const *data = &menus->inputs[input]; + + inputs[count++].function = (short)game_data_input_index(data->input, data->file, data->line); + } + } + /* search and replace */ + for (count = 0, replace = source->first_replace; replace != HALO_MENU_NONE; replace = menus->replaces[replace].next) + count++; + if (count) + { + struct ui_widget_search_and_replace_reference *replaces = allocate(count * sizeof(*replaces)); + + definition->search_and_replace_functions.count = count; + definition->search_and_replace_functions.address = replaces; + for (count = 0, replace = source->first_replace; replaces && replace != HALO_MENU_NONE; + replace = menus->replaces[replace].next) + { + struct halo_menu_replace const *item = &menus->replaces[replace]; + long function = item->function ? name_index(item->function, replace_names, NUMBEROF(replace_names)) : 0; + + if (function == NONE) + problem(item->file, item->line, "a replace function is none or controller:", item->function); + strncpy(replaces[count].search_string, item->search, sizeof(replaces[count].search_string) - 1); + /* (the PC version's own are none here) */ + replaces[count++].replace_function = (short)(function == 1 ? 1 : 0); + } + } + /* the text */ + reference_clear(&definition->text_label_string_list, UNICODE_STRING_LIST_TAG); + reference_clear(&definition->text_font, FONT_TAG); + if ((source->text != NULL) + (source->strings != NULL) + (source->string_list != NULL) > 1) + problem(source->file, source->line, "a widget has one of text, strings and string_list:", source->name); + if (build.text_tags[widget_index] != NONE) + { + reference_set(&definition->text_label_string_list, UNICODE_STRING_LIST_TAG, build.text_tags[widget_index]); + } + else if (build.spinner_tags[widget_index] != NONE) + { + reference_set(&definition->text_label_string_list, UNICODE_STRING_LIST_TAG, build.spinner_tags[widget_index]); + definition->list_flags |= FLAG(_list_items_generated_from_string_list_tag_bit); + } + else if (source->string_list) + { + reference_set(&definition->text_label_string_list, UNICODE_STRING_LIST_TAG, + tag_named(UNICODE_STRING_LIST_TAG, source->string_list, source->file, source->line)); + } + definition->string_list_index = (short)source->string_index; + /* (our own text is white in the large font, unless given) */ + if (source->text || source->strings) + { + definition->text_color.alpha = definition->text_color.red = 1.0f; + definition->text_color.green = definition->text_color.blue = 1.0f; + if (!source->font) + reference_set(&definition->text_font, FONT_TAG, font_tag("large", source->file, source->line)); + } + if (source->color && !parse_color(source->color, &definition->text_color)) + problem(source->file, source->line, "a color is #RRGGBB or #AARRGGBB:", source->color); + if (source->font) + reference_set(&definition->text_font, FONT_TAG, font_tag(source->font, source->file, source->line)); + if (source->align) + { + long align = name_index(source->align, align_names, NUMBEROF(align_names)); + + if (align == NONE) + problem(source->file, source->line, "align is \"left\", \"right\" or \"center\":", source->align); + else + definition->justification = (short)align; + } + definition->text_box_flags = (word)flags_parse(source->text_flags, text_flag_names, NUMBEROF(text_flag_names), + source->file, source->line); + definition->horizontal_offset = (short)source->text_x; + definition->vertical_offset = (short)source->text_y; + /* the list */ + definition->list_flags |= flags_parse(source->list_flags, list_flag_names, NUMBEROF(list_flag_names), + source->file, source->line); + if (source->strings && (definition->type != _widget_type_spinner_list || source->first_child != HALO_MENU_NONE)) + problem(source->file, source->line, "strings are for a spinner with no children:", source->name); + if (TEST_FLAG(definition->list_flags, _list_items_generated_from_string_list_tag_bit) && + (definition->type != _widget_type_spinner_list || source->first_child != HALO_MENU_NONE || + definition->text_label_string_list.index == NONE)) + problem(source->file, source->line, "items_from_strings is for a spinner with strings and no children:", + source->name); + if (source->setting) + { + if (!source->strings || !source->values) + problem(source->file, source->line, "a setting needs strings and values:", source->name); + else + setting_add(source, build.widget_tags[widget_index]); + } + reference_clear(&definition->list_header_bitmap, BITMAP_GROUP_TAG); + reference_clear(&definition->list_footer_bitmap, BITMAP_GROUP_TAG); + if (source->header_bitmap) + { + reference_set(&definition->list_header_bitmap, BITMAP_GROUP_TAG, + tag_named(BITMAP_GROUP_TAG, source->header_bitmap, source->file, source->line)); + } + if (source->footer_bitmap) + { + reference_set(&definition->list_footer_bitmap, BITMAP_GROUP_TAG, + tag_named(BITMAP_GROUP_TAG, source->footer_bitmap, source->file, source->line)); + } + parse_bounds(source->header_bounds, &definition->list_header_bounds, source->file, source->line); + parse_bounds(source->footer_bounds, &definition->list_footer_bounds, source->file, source->line); + reference_clear(&definition->extended_description_widget, UI_WIDGET_DEFINITION_TAG); + if (source->description) + { + if (definition->type != _widget_type_column_list) + problem(source->file, source->line, "a description is for a column list:", source->name); + reference_set(&definition->extended_description_widget, UI_WIDGET_DEFINITION_TAG, + tag_named(UI_WIDGET_DEFINITION_TAG, source->description, source->file, source->line)); + } + /* the children */ + for (count = 0, child = source->first_child; child != HALO_MENU_NONE; child = menus->children[child].next) + count++; + if (definition->type == _widget_type_spinner_list && count != 0 && count != 1 && count != 3) + problem(source->file, source->line, "a spinner has 0, 1 or 3 children:", source->name); + if (count) + { + struct ui_widget_child_reference *children = allocate(count * sizeof(*children)); + + definition->child_widgets.count = count; + definition->child_widgets.address = children; + for (count = 0, child = source->first_child; children && child != HALO_MENU_NONE; child = menus->children[child].next) + { + struct halo_menu_child const *entry = &menus->children[child]; + long tag_index = entry->nested != HALO_MENU_NONE ? build.widget_tags[entry->nested] : + tag_named(UI_WIDGET_DEFINITION_TAG, entry->widget, entry->file, entry->line); + char const *name = entry->nested != HALO_MENU_NONE ? menus->widgets[entry->nested].name : entry->widget; + char const *child_leaf = strrchr(name, '/') ? strrchr(name, '/') + 1 : name; + + reference_set(&children[count].widget_tag, UI_WIDGET_DEFINITION_TAG, tag_index); + strncpy(children[count].name, child_leaf, sizeof(children[count].name) - 1); + children[count].horizontal_offset = (short)entry->x; + children[count].vertical_offset = (short)entry->y; + if (entry->controller) + { + if (strlen(entry->controller) != 1 || entry->controller[0] < '1' || entry->controller[0] > '4') + problem(entry->file, entry->line, "a child's controller is 1 to 4:", entry->controller); + SET_FLAG(children[count].flags, _child_widget_use_custom_controller_index_bit, TRUE); + children[count].custom_controller_index = (short)(entry->controller[0] - '1'); + } + count++; + } + } + /* the conditional widgets */ + for (conditional = source->first_conditional; conditional != HALO_MENU_NONE; + conditional = menus->conditionals[conditional].next) + { + struct halo_menu_conditional const *item = &menus->conditionals[conditional]; + + conditional_add(definition, tag_named(UI_WIDGET_DEFINITION_TAG, item->widget, item->file, item->line), + item->if_failed ? 1 : 0); + } + /* the event handlers: one for each of each 's events */ + for (count = 0, handler = source->first_handler; handler != HALO_MENU_NONE; handler = menus->handlers[handler].next) + count++; + if (count) + { + long total = 0; + + /* (count the events) */ + for (handler = source->first_handler; handler != HALO_MENU_NONE; handler = menus->handlers[handler].next) + { + char const *text = menus->handlers[handler].event; + + while (text && *text) + { + long length; + + text += strspn(text, " \t\r\n"); + length = (long)strcspn(text, " \t\r\n"); + if (!length) + break; + total++; + text += length; + } + } + if (total) + { + struct ui_widget_event_handler_reference *handlers = allocate(total * sizeof(*handlers)); + + definition->event_handlers.count = total; + definition->event_handlers.address = handlers; + for (count = 0, handler = source->first_handler; handlers && handler != HALO_MENU_NONE; + handler = menus->handlers[handler].next) + { + struct halo_menu_handler const *on = &menus->handlers[handler]; + char const *text = on->event; + + while (text && *text) + { + char word[64]; + long length, event; + + text += strspn(text, " \t\r\n"); + length = (long)strcspn(text, " \t\r\n"); + if (!length) + break; + if (length >= (long)sizeof(word)) + length = sizeof(word) - 1; + memcpy(word, text, length); + word[length] = 0; + text += strcspn(text, " \t\r\n"); + event = name_index(word, event_names, NUMBEROF(event_names)); + if (event == NONE) + problem(on->file, on->line, "there is no event", word); + handler_build(&handlers[count++], on, event, definition); + } + } + } + } + return definition; +} + +/* the tag table with count more entries, the existing ones as they are */ +static struct cache_file_tag_instance *instances_grow(long count, long *first_index, long *first_salt) +{ + long existing, index, salt = 0; + struct cache_file_tag_instance *instances = cache_files_tag_instances(&existing); + struct cache_file_tag_instance *grown; + + if (!instances) + return NULL; + grown = allocate((existing + count) * sizeof(*grown)); + if (!grown) + return NULL; + memcpy(grown, instances, existing * sizeof(*grown)); + for (index = 0; index < existing; index++) + { + long instance_salt = (unsigned long)instances[index].tag_index >> 16; + + if (instance_salt > salt) + salt = instance_salt; + } + menu_tags.original_instances = instances; + menu_tags.original_count = existing; + *first_index = existing; + *first_salt = salt + 1; + return grown; +} + +static long next_tag(void) +{ + long tag_index = ((build.first_salt + build.next) << 16) | (build.first_index + build.next); + + build.next++; + return tag_index; +} + +static void instance_set(struct cache_file_tag_instance *instances, long group_tag, long tag_index, char const *name, + char const *suffix, void *definition) +{ + struct cache_file_tag_instance *instance = &instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(tag_index)]; + char *copy = allocate((long)strlen(PC_MENU_TAG_PREFIX) + (long)strlen(name) + (long)strlen(suffix) + 1); + + if (copy) + { + char *character; + + strcpy(copy, PC_MENU_TAG_PREFIX); + strcat(copy, name); + strcat(copy, suffix); + /* (named as the map's tags are) */ + for (character = copy; *character; character++) + { + if (*character == '/') + *character = '\\'; + } + } + instance->group_tag = group_tag; + instance->parent_group_tags[0] = NONE; + instance->parent_group_tags[1] = NONE; + instance->tag_index = tag_index; + instance->name = copy; + instance->base_address = definition; +} + +static void menu_tags_release(void) +{ + long index; + + for (index = 0; index < menu_tags.bitmap_count; index++) + { + if (menu_tags.bitmaps[index]->hardware_format) + rasterizer_bitmap_delete(menu_tags.bitmaps[index]); + } + halo_menus_art_forget(); + if (menu_tags.original_instances) + cache_files_set_tag_instances(menu_tags.original_instances, menu_tags.original_count); + /* (the game's free takes no NULL: settings is, with no setting spinners) */ + for (index = 0; index < menu_tags.block_count; index++) + free(menu_tags.blocks[index]); + if (menu_tags.blocks) + free(menu_tags.blocks); + if (menu_tags.bitmaps) + free(menu_tags.bitmaps); + if (menu_tags.settings) + free(menu_tags.settings); + memset(&menu_tags, 0, sizeof(menu_tags)); +} + +/* ---------- public code */ + +void menu_tags_loaded( + char const *map_name) +{ + struct halo_menus const *menus; + struct cache_file_tag_instance *instances; + long widget_count, own_lists = 0, total, index; + + if (strcmp(map_name, "ui") || strcmp(config_string("display.menus"), "pc")) + return; + menus = halo_menus_load(); + if (!menus) + return; + memset(&build, 0, sizeof(build)); + build.menus = menus; + widget_count = menus->widget_count; + build.widget_tags = malloc((widget_count + 1) * sizeof(long)); + build.text_tags = malloc((widget_count + 1) * sizeof(long)); + build.spinner_tags = malloc((widget_count + 1) * sizeof(long)); + build.bitmap_tags = malloc((menus->bitmap_count + 1) * sizeof(long)); + build.strings_tags = malloc((menus->string_list_count + 1) * sizeof(long)); + for (index = 0; index < widget_count; index++) + { + own_lists += (menus->widgets[index].text != NULL) + (menus->widgets[index].strings != NULL); + /* (names are unique) */ + if (widget_named(menus->widgets[index].name) != index) + problem(menus->widgets[index].file, menus->widgets[index].line, "two widgets are named", + menus->widgets[index].name); + } + total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count; + instances = build.failed ? NULL : instances_grow(total, &build.first_index, &build.first_salt); + if (!instances) + goto failed; + /* each new tag's index */ + for (index = 0; index < widget_count; index++) + { + build.widget_tags[index] = next_tag(); + build.text_tags[index] = menus->widgets[index].text ? next_tag() : NONE; + build.spinner_tags[index] = menus->widgets[index].strings ? next_tag() : NONE; + } + for (index = 0; index < menus->string_list_count; index++) + build.strings_tags[index] = next_tag(); + for (index = 0; index < menus->bitmap_count; index++) + build.bitmap_tags[index] = next_tag(); + /* the tags: the bitmaps and strings first, which the widgets name; the + widgets' names in the table before any is built, which they find there */ + for (index = 0; index < menus->bitmap_count && !build.failed; index++) + { + instance_set(instances, BITMAP_GROUP_TAG, build.bitmap_tags[index], menus->bitmaps[index].name, "", + bitmap_build(&menus->bitmaps[index], build.bitmap_tags[index])); + } + for (index = 0; index < menus->string_list_count && !build.failed; index++) + { + struct halo_menu_strings const *list = &menus->string_lists[index]; + + instance_set(instances, UNICODE_STRING_LIST_TAG, build.strings_tags[index], list->name, "", + string_list_build(menus->strings + list->first, list->count)); + } + for (index = 0; index < widget_count && !build.failed; index++) + { + struct halo_menu_widget const *widget = &menus->widgets[index]; + + if (widget->text) + { + instance_set(instances, UNICODE_STRING_LIST_TAG, build.text_tags[index], widget->name, " text", + string_list_build(&widget->text, 1)); + } + if (widget->strings) + { + char const *pieces[MAXIMUM_STRINGS]; + long count = split(widget->strings, pieces); + + instance_set(instances, UNICODE_STRING_LIST_TAG, build.spinner_tags[index], widget->name, " strings", + string_list_build(pieces, count)); + } + instance_set(instances, UI_WIDGET_DEFINITION_TAG, build.widget_tags[index], widget->name, "", NULL); + } + if (build.failed) + goto failed; + cache_files_set_tag_instances(instances, build.first_index + total); + for (index = 0; index < widget_count && !build.failed; index++) + instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index); + if (build.failed) + goto failed; + if (widget_named(menus->root) != NONE) + { + snprintf(menu_tags.root, sizeof(menu_tags.root), "%s%s", PC_MENU_TAG_PREFIX, menus->root); + for (index = 0; menu_tags.root[index]; index++) + { + if (menu_tags.root[index] == '/') + menu_tags.root[index] = '\\'; + } + } + else + { + platform_log("menus: there is no widget named %s, the main menu", menus->root); + } + menu_tags.loaded = TRUE; + platform_log("menus: %ld widgets, %ld string lists and %ld bitmaps added to the map's %ld tags", + widget_count, own_lists + menus->string_list_count, menus->bitmap_count, build.first_index); + goto done; + +failed: + platform_log("menus: not added; using the game's own menus"); + menu_tags_release(); + +done: + free(build.widget_tags); + free(build.text_tags); + free(build.spinner_tags); + free(build.strings_tags); + free(build.bitmap_tags); + memset(&build, 0, sizeof(build)); +} + +void menu_tags_unloaded( + void) +{ + if (menu_tags.loaded || menu_tags.block_count) + menu_tags_release(); +} + +char const *pc_menus_root_name( + void) +{ + /* (debug.menu_open: a screen to see, a profile being edited for the + settings' screens) */ + char const *open = config_string("debug.menu_open"); + + if (menu_tags.loaded && *open && build.menus == NULL) + { + static char name[300]; + long index; + + snprintf(name, sizeof(name), "%s%s", PC_MENU_TAG_PREFIX, open); + for (index = 0; name[index]; index++) + { + if (name[index] == '/') + name[index] = '\\'; + } + if (tag_loaded(UI_WIDGET_DEFINITION_TAG, name) != NONE) + { + extern boolean pc_menu_profile_edit_begin(void); + + pc_menu_profile_edit_begin(); + return name; + } + platform_log("menus: debug.menu_open: there is no widget named %s", open); + } + return menu_tags.loaded && menu_tags.root[0] ? menu_tags.root : "ui\\shell\\main_menu\\main_menu"; +} + +boolean pc_menu_tag( + long tag_index) +{ + return menu_tags.loaded && tag_index != NONE && + DATUM_INDEX_TO_ABSOLUTE_INDEX(tag_index) >= menu_tags.original_count; +} + +char const *pc_menu_function_name( + long function_index) +{ + return function_index >= 0 && function_index < (long)NUMBEROF(port_function_names) ? + port_function_names[function_index] : NULL; +} + +/* a widget definition's string_index (port settings help: its label's) */ +short pc_menu_string_index( + long definition_index) +{ + struct ui_widget_definition *definition = tag_get(UI_WIDGET_DEFINITION_TAG, definition_index); + + return definition ? definition->string_list_index : NONE; +} + +/* the screen the engine opens by name after a network game (the Xbox's), +or ours where the PC version's menus are up: the host's map select, the +lobby */ +char const *pc_menus_screen( + char const *name) +{ + static struct + { + char const *game_name; + char const *menu_name; + } const screens[] = + { + { "ui\\shell\\main_menu\\multiplayer_type_select\\connected\\connected_map_select_postgame_wrapper", + "pc\\main_menu\\multiplayer_type_select\\connected\\connected_map_select_wrapper" }, + { "ui\\shell\\main_menu\\multiplayer_type_select\\connected\\pregame\\connected_pregame_screen", + "pc\\main_menu\\multiplayer_type_select\\lobby\\lobby_screen" }, + }; + short index; + + if (!menu_tags.loaded) + return name; + for (index = 0; index < NUMBEROF(screens); index++) + { + if (!strcmp(name, screens[index].game_name)) + return screens[index].menu_name; + } + return name; +} + +char const *pc_menu_game_data_input_name( + long function_index) +{ + return function_index >= 0 && function_index < (long)NUMBEROF(port_game_data_input_names) ? + port_game_data_input_names[function_index] : NULL; +} + +struct pc_menu_setting *pc_menu_setting_get( + long definition_index) +{ + long index; + + for (index = 0; index < menu_tags.setting_count; index++) + { + if (menu_tags.settings[index].definition_index == definition_index) + return &menu_tags.settings[index]; + } + return NULL; +} diff --git a/port/linux/game/render_interpolation.c b/port/linux/game/render_interpolation.c index 95b29a4b..b007f84e 100644 --- a/port/linux/game/render_interpolation.c +++ b/port/linux/game/render_interpolation.c @@ -41,6 +41,7 @@ with their unit, and with what it rides. /* port/linux/src/port_config.c */ int config_boolean(const char *name); +unsigned long config_changes(void); #include #include @@ -672,12 +673,16 @@ static struct observer_result const *render_interpolation_direct_camera( (void)local_player_index; return observer; #else - static int enabled = -1; + static int enabled; + static unsigned long read_at = (unsigned long)-1; struct observer_result *direct; long unit_index; - if (enabled < 0) + if (read_at != config_changes()) + { + read_at = config_changes(); enabled = config_boolean("display.direct_camera"); + } if (!enabled || !observer || director_get_perspective(local_player_index) != _director_perspective_first_person || director_inhibited_facing(local_player_index) || diff --git a/port/linux/include/halo_keyboard.h b/port/linux/include/halo_keyboard.h new file mode 100644 index 00000000..e8a66d12 --- /dev/null +++ b/port/linux/include/halo_keyboard.h @@ -0,0 +1,45 @@ +/* +HALO_KEYBOARD.H + +The keyboard and mouse's own controls (port/linux/src/xinput_sdl.c): in the +game they do not drive the first controller, as they do in the menus, but +the player's actions themselves, by the bindings in config.toml's +[controls] (Settings > Controls Setup changes them). The game takes them +with the first controller's (source/input/input_abstraction.c), so the +profile's button layout is the controller's alone, and reloading has a key +of its own (source/game/player_control.c). +*/ + +#ifndef HALO_KEYBOARD_H +#define HALO_KEYBOARD_H + +enum halo_keyboard_action +{ + HALO_KEYBOARD_MOVE_FORWARD, + HALO_KEYBOARD_MOVE_BACKWARD, + HALO_KEYBOARD_STRAFE_LEFT, + HALO_KEYBOARD_STRAFE_RIGHT, + HALO_KEYBOARD_JUMP, + HALO_KEYBOARD_CROUCH, + HALO_KEYBOARD_FIRE, + HALO_KEYBOARD_THROW_GRENADE, + HALO_KEYBOARD_MELEE, + HALO_KEYBOARD_RELOAD, + HALO_KEYBOARD_ZOOM, + HALO_KEYBOARD_SWITCH_WEAPON, + HALO_KEYBOARD_SWITCH_GRENADE, + HALO_KEYBOARD_ACTION, + HALO_KEYBOARD_FLASHLIGHT, + /* (these two press the controller's Back and Start, which the menus + read from it) */ + HALO_KEYBOARD_SCOREBOARD, + HALO_KEYBOARD_PAUSE, + NUMBER_OF_HALO_KEYBOARD_ACTIONS +}; + +/* the actions the keyboard and mouse hold for the player on the controller +(a bit for each halo_keyboard_action): theirs is the first's; none while a +menu or the console is up */ +unsigned long halo_keyboard_actions(short controller_index); + +#endif diff --git a/port/linux/include/halo_menus.h b/port/linux/include/halo_menus.h new file mode 100644 index 00000000..abcf7f43 --- /dev/null +++ b/port/linux/include/halo_menus.h @@ -0,0 +1,154 @@ +/* +HALO_MENUS.H + +The menus written in XML (port/assets/menus, and a menus folder beside +config.toml), as the platform layer reads them (port/linux/src/menu_files.c) +for the game, which builds them into widget tags when ui.map loads +(port/linux/game/menu_tags.c). Plain C89 types, for both: each element keeps +its attributes as written, and the file and line it came from; the game reads +their meaning. + +A file holds with (holding s), (holding +s) and elements; a holds and +(its children), (an event handler), (a game data input), + and elements. port/assets/menus/README.md describes +them. +*/ + +#ifndef HALO_MENUS_H +#define HALO_MENUS_H + +#define HALO_MENU_NONE (-1L) +/* the port's event handler and game data functions (menu_functions.c) are +numbered from here, clear of the Xbox's (0 to 101 and 0 to 40) and the PC's +(to 189 and 58) */ +#define PC_MENU_FUNCTION_BASE 256 + +/* a frame: a PNG drawn at width by height, or the map's bitmap's frame +(map: its bitmap group tag, index: the bitmap's number in it) */ +struct halo_menu_frame +{ + char const *png; + char const *map; + long index; + long width, height; +}; + +struct halo_menu_bitmap +{ + char const *name; + /* its frames: frame_count from first_frame in the frames */ + long first_frame, frame_count; + char const *file; + long line; +}; + +struct halo_menu_strings +{ + char const *name; + /* its strings: count from first in the strings */ + long first, count; + char const *file; + long line; +}; + +struct halo_menu_handler +{ + char const *event, *run, *script, *open, *replace, *close, *widget, *focus, *reload, *sound, *otherwise, *label; + long back, branch; + long next; + char const *file; + long line; +}; + +struct halo_menu_input +{ + char const *input; + long next; + char const *file; + long line; +}; + +/* a child: a inside its parent (nested), or one named () */ +struct halo_menu_child +{ + long nested; + char const *widget; + long x, y; + char const *controller; + long next; + char const *file; + long line; +}; + +struct halo_menu_conditional +{ + char const *widget; + long if_failed; + long next; + char const *file; + long line; +}; + +struct halo_menu_replace +{ + char const *search, *function; + long next; + char const *file; + long line; +}; + +struct halo_menu_widget +{ + char const *name, *type, *controller, *flags, *bitmap, *text, *strings, *values, *setting, *font, *color, + *align, *description, *string_list, *list_flags, *text_flags, *header_bitmap, *footer_bitmap, + *header_bounds, *footer_bounds; + long x, y, left, top, width, height, text_x, text_y, auto_close, auto_close_fade, string_index; + long has_width, has_height; + /* indices in their arrays, or HALO_MENU_NONE */ + long parent, first_child, first_handler, first_input, first_conditional, first_replace; + char const *file; + long line; +}; + +struct halo_menus +{ + struct halo_menu_bitmap *bitmaps; + long bitmap_count; + struct halo_menu_frame *frames; + long frame_count; + struct halo_menu_strings *string_lists; + long string_list_count; + char const **strings; + long string_count; + struct halo_menu_widget *widgets; + long widget_count; + struct halo_menu_child *children; + long child_count; + struct halo_menu_handler *handlers; + long handler_count; + struct halo_menu_input *inputs; + long input_count; + struct halo_menu_conditional *conditionals; + long conditional_count; + struct halo_menu_replace *replaces; + long replace_count; + /* the main menu: (the last given), else "main_menu" */ + char const *root; +}; + +/* the menus, read from the files (once; the same each call after); NULL if +there are none, or a file has an error (logged) */ +struct halo_menus const *halo_menus_load(void); +/* text as UTF-16, its terminator included, "\n" written as the game's line +breaks ("\r\n"): the number of characters written (at most capacity) */ +long halo_menus_utf16(char const *utf8, unsigned short *out, long capacity); +/* logs a problem in a file's element */ +void halo_menus_log(char const *file, long line, char const *message, char const *detail); + +/* menu art: the D3D texture (as the game's bitmap holds it) a frame's PNG is +drawn for, until halo_menus_art_forget */ +void halo_menus_art_register(void const *texture, char const *png); +void halo_menus_art_forget(void); + +#endif diff --git a/port/linux/include/halo_port_limits.h b/port/linux/include/halo_port_limits.h index ae7ee966..92571c49 100644 --- a/port/linux/include/halo_port_limits.h +++ b/port/linux/include/halo_port_limits.h @@ -46,8 +46,10 @@ machines, 16 players) are 0x226 and 0x434. */ #define HALO_PORT_NETWORK_GAME_PLAYERS_END \ (HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + HALO_PORT_MAXIMUM_NETWORK_PLAYERS * HALO_PORT_NETWORK_PLAYER_SIZE) #define HALO_PORT_NETWORK_GAME_RANDOM_SEED_OFFSET (HALO_PORT_NETWORK_GAME_PLAYERS_END + 2) -#define HALO_PORT_NETWORK_GAME_LOCAL_DATA_OFFSET (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0xA) -#define HALO_PORT_NETWORK_GAME_SIZE (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0xE) +/* (then the gametype's PC options, struct game_variant_options: 0x1C bytes) */ +#define HALO_PORT_NETWORK_GAME_VARIANT_OPTIONS_OFFSET (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0xA) +#define HALO_PORT_NETWORK_GAME_LOCAL_DATA_OFFSET (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0x26) +#define HALO_PORT_NETWORK_GAME_SIZE (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0x2A) /* ---------- system link protocol @@ -64,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode, in its game's advertisement's reserved bytes (network_server_message_handler.c), which hosts built before there was a version send as zeros: version 0. Raise it with any change to what the machines send each other. */ -#define HALO_PORT_NETWORK_VERSION 10 +#define HALO_PORT_NETWORK_VERSION 11 /* ... the advertisement's reserved bytes: the version (a little-endian word), then flags */ #define HALO_PORT_ADVERTISED_VERSION_OFFSET 0 @@ -72,6 +74,9 @@ then flags */ /* ... the host plays the distributed netcode (always, since the lockstep netcode was removed; hosts of version 4 built before then may not) */ #define HALO_PORT_ADVERTISED_DISTRIBUTED_FLAG 0x01 +/* ... the game is under way (loading, playing or over), not in its lobby: +the menus show it before joining it (hosts built before then never set it) */ +#define HALO_PORT_ADVERTISED_IN_PROGRESS_FLAG 0x02 /* a message header's 12-bit length allows messages of up to 0xFFF bytes, header included; the per-tick update of 128 players is 3,857 */ @@ -81,7 +86,7 @@ header included; the per-tick update of 128 players is 3,857 */ update of 128 players decodes to 0x1010 bytes */ #define HALO_PORT_NETWORK_PACKET_SIZE 0x1100 -/* the game settings record (HALO_PORT_NETWORK_GAME_SIZE, 13,092 bytes at 128 +/* the game settings record (HALO_PORT_NETWORK_GAME_SIZE, 13,120 bytes at 128 machines and players) does not fit one message; it is sent in pieces of this many bytes (4 pieces), each 3,594 bytes on the wire */ #define HALO_PORT_NETWORK_GAME_SETTINGS_FRAGMENT_SIZE 0xE00 diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 06ff2b24..43c924d2 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -1154,8 +1154,8 @@ HRESULT WINAPI Direct3D_CreateDevice(UINT adapter, D3DDEVTYPE device_type, void #ifdef HALO_ANDROID int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer) { - (void)menus_active; (void)pointer; + platform_menus_set_active(menus_active != 0); return 0; } #else @@ -1194,6 +1194,7 @@ int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer) { struct platform_ui_pointer state; + platform_menus_set_active(menus_active != 0); platform_ui_pointer_set_active(menus_active != 0); if (!menus_active || !device.gl_ready || !platform_ui_pointer_read(&state)) return 0; diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 98dc1226..09af3d76 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -720,6 +720,15 @@ ULONG WINAPI IDirectSound_Release(LPDIRECTSOUND sound) VOID WINAPI DirectSoundDoWork(void) { + static unsigned long volume_read_at = (unsigned long)-1; + + /* (audio.volume read again when Settings changes it: on the game's + thread, not the mixer's, whose lock the config's file I/O would hold) */ + if (volume_read_at != config_changes()) + { + volume_read_at = config_changes(); + master_volume = (float)config_real("audio.volume"); + } streams_complete_finished(); } diff --git a/port/linux/src/hud_hires.c b/port/linux/src/hud_hires.c index d709207f..acfa87fc 100644 --- a/port/linux/src/hud_hires.c +++ b/port/linux/src/hud_hires.c @@ -69,11 +69,13 @@ long hud_hires_asset_fits(long asset, long width, long height) long hud_hires_override_find(unsigned long address, unsigned long width, unsigned long height, unsigned long level0_size) { - static int hud_enabled = -1, titles_enabled = -1; + static int hud_enabled, titles_enabled; + static unsigned long read_at = (unsigned long)-1; long asset; - if (hud_enabled < 0) + if (read_at != config_changes()) { + read_at = config_changes(); hud_enabled = config_boolean("display.high_res_hud"); titles_enabled = config_boolean("display.high_res_text"); } @@ -121,13 +123,14 @@ static unsigned char paeth(unsigned char left, unsigned char up, unsigned char u return to_up <= to_up_left ? up : up_left; } -/* the PNG's texels, RGBA in rows top first; NULL if it is not one that -tools/hud_assets.py writes */ -static unsigned char *png_decode(const struct hud_hires_embedded *embedded) +/* the PNG's texels, RGBA in rows top first, and its size; NULL if it is not +one that tools/hud_assets.py writes */ +static unsigned char *png_decode(const unsigned char *data, unsigned long size, unsigned long *png_width, + unsigned long *png_height) { - const unsigned char *data = (const unsigned char *)embedded->png; - unsigned long size = embedded->png_size, position = 8; - unsigned long width = embedded->width, height = embedded->height; + unsigned long position = 8; + unsigned long width = size >= 33 ? big_endian_long(data + 16) : 0; + unsigned long height = size >= 33 ? big_endian_long(data + 20) : 0; unsigned long stride = width * 4, filtered_size = height * (stride + 1); unsigned char *compressed = NULL, *filtered = NULL, *pixels = NULL; unsigned long compressed_size = 0, row, column; @@ -135,9 +138,11 @@ static unsigned char *png_decode(const struct hud_hires_embedded *embedded) int result; if (size < 33 || memcmp(data, "\x89PNG\r\n\x1a\n", 8) || memcmp(data + 12, "IHDR", 4) || - big_endian_long(data + 16) != width || big_endian_long(data + 20) != height || + !width || !height || width > 8192 || height > 8192 || data[24] != 8 || data[25] != 6 || data[28] != 0) return NULL; + *png_width = width; + *png_height = height; compressed = malloc(size); while (compressed && position + 12 <= size) { @@ -203,12 +208,33 @@ failed: return NULL; } +unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned long *levels) +{ + unsigned long width = 0, height = 0, largest; + unsigned char *pixels = png_decode(png, size, &width, &height); + GLuint texture; + + if (!pixels) + return 0; + *levels = 1; + for (largest = width > height ? width : height; largest > 1; largest >>= 1) + (*levels)++; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + xgpu_gl_state_invalidate(); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, (GLint)*levels - 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + glGenerateMipmap(GL_TEXTURE_2D); + xgpu_gl_state_invalidate(); + free(pixels); + return texture; +} + unsigned int hud_hires_override_texture(long asset, unsigned long *levels) { const struct hud_hires_embedded *embedded; - unsigned char *pixels; - unsigned long largest; - GLuint texture; if (asset < 0 || asset >= hud_hires_asset_count() || textures[asset].failed) return 0; @@ -218,28 +244,13 @@ unsigned int hud_hires_override_texture(long asset, unsigned long *levels) return textures[asset].texture; } embedded = &hud_hires_embedded[asset]; - pixels = png_decode(embedded); - if (!pixels) + textures[asset].texture = hud_hires_png_texture(embedded->png, embedded->png_size, &textures[asset].levels); + if (!textures[asset].texture) { platform_log("high-res hud: could not decode the texture for %s bitmap %d", embedded->tag, embedded->bitmap); textures[asset].failed = 1; return 0; } - textures[asset].levels = 1; - for (largest = embedded->width > embedded->height ? embedded->width : embedded->height; largest > 1; largest >>= 1) - textures[asset].levels++; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - xgpu_gl_state_invalidate(); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, (GLint)textures[asset].levels - 1); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, (GLsizei)embedded->width, (GLsizei)embedded->height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, pixels); - glGenerateMipmap(GL_TEXTURE_2D); - xgpu_gl_state_invalidate(); - free(pixels); - textures[asset].texture = texture; *levels = textures[asset].levels; - return texture; + return textures[asset].texture; } diff --git a/port/linux/src/hud_hires.h b/port/linux/src/hud_hires.h index 432260b8..ec8cb56e 100644 --- a/port/linux/src/hud_hires.h +++ b/port/linux/src/hud_hires.h @@ -48,6 +48,9 @@ long hud_hires_override_find(unsigned long address, unsigned long width, unsigne /* its GL texture (decoded and uploaded, mipmapped, on first use; 0 if it could not be), and the number of its mip levels */ unsigned int hud_hires_override_texture(long asset, unsigned long *levels); +/* a GL texture drawn from an 8-bit RGBA PNG (as the tools write them), with +all its mip levels, and their number; 0 if it could not be */ +unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned long *levels); /* whether its green is its coverage (d3d8_gl.c, nv2a_psh.c: coverage_alpha) */ int hud_hires_override_coverage(long asset); diff --git a/port/linux/src/menu_files.c b/port/linux/src/menu_files.c new file mode 100644 index 00000000..09ebcc99 --- /dev/null +++ b/port/linux/src/menu_files.c @@ -0,0 +1,988 @@ +/* +MENU_FILES.C + +The menus written in XML (halo_menus.h): the files embedded from +port/assets/menus (menu_files.h), each replaced by a file of the same name +in a menus folder beside config.toml, and on the desktop any other .xml file +there too, read with Expat (port/third_party/expat) into the description the +game builds its widget tags from (port/linux/game/menu_tags.c). + +Only the files' shape is checked here: their elements, which attributes +each may have, and numbers; the game checks what the names mean, against +the loaded map. Any problem is logged with its file and line, and then no +file is used: the game keeps its own menus. + +Also the menus' art: the PNGs of their bitmaps' frames, each drawn for the +D3D texture the game made for it (menu_art_texture, xbox_textures.c), as the +high-res HUD's are (hud_hires.c). +*/ + +#include "halo_menus.h" +#include "hud_hires.h" +#include "menu_files.h" +#include "platform.h" +#include "port_config.h" +#include "xgpu.h" + +#include "expat.h" + +#include +#include +#include +#include +#include +#include + +#define MAXIMUM_DEPTH 32 +#define MAXIMUM_ART 1024 + +/* ---------- the files */ + +struct menu_file +{ + char *path; + const unsigned char *data; + unsigned long size; + int loaded; +}; + +static struct menu_file *files; +static long file_count; +static char folder[1024]; + +static long file_find(const char *path) +{ + long index; + + for (index = 0; index < file_count; index++) + { + if (!strcmp(files[index].path, path)) + return index; + } + return -1; +} + +static void file_add(const char *path, const unsigned char *data, unsigned long size, int loaded) +{ + long index = file_find(path); + + if (index < 0) + { + files = realloc(files, (file_count + 1) * sizeof(*files)); + index = file_count++; + files[index].path = strdup(path); + } + else if (files[index].loaded) + { + free((void *)files[index].data); + } + files[index].data = data; + files[index].size = size; + files[index].loaded = loaded; +} + +static unsigned char *file_read(const char *path, unsigned long *size) +{ +#ifdef HALO_ANDROID + FILE *file = fopen(path, "rb"); + unsigned char *data = NULL; + long length; + + if (!file) + return NULL; + if (fseek(file, 0, SEEK_END) == 0 && (length = ftell(file)) >= 0 && fseek(file, 0, SEEK_SET) == 0) + { + data = malloc((size_t)length + 1); + if (data && fread(data, 1, (size_t)length, file) == (size_t)length) + { + *size = (unsigned long)length; + } + else + { + free(data); + data = NULL; + } + } + fclose(file); + return data; +#else + size_t length = 0; + void *loaded = SDL_LoadFile(path, &length); + unsigned char *data; + + if (!loaded) + return NULL; + data = malloc(length + 1); + if (data) + memcpy(data, loaded, length); + SDL_free(loaded); + *size = (unsigned long)length; + return data; +#endif +} + +/* the embedded files, then those of the menus folder: one of the same name +replaces an embedded one, and (on the desktop, where the folder can be +listed) another .xml file is added */ +static void files_gather(void) +{ + unsigned int index; + char path[1200]; + + config_folder(folder, sizeof(folder)); + snprintf(folder + strlen(folder), sizeof(folder) - strlen(folder), "menus"); + for (index = 0; index < menu_files_embedded_count; index++) + { + const struct menu_file_embedded *embedded = &menu_files_embedded[index]; + unsigned long size = 0; + unsigned char *data; + + snprintf(path, sizeof(path), "%s/%s", folder, embedded->path); + data = file_read(path, &size); + if (data) + { + platform_log("menus: %s replaces the built-in one", path); + file_add(embedded->path, data, size, 1); + } + else + { + file_add(embedded->path, (const unsigned char *)embedded->data, embedded->size, 0); + } + } +#ifndef HALO_ANDROID + { + /* (the folder's own and its folders': SDL's * does not cross a /) */ + static const char *const patterns[] = { "*.xml", "*/*.xml" }; + int pattern; + + for (pattern = 0; pattern < 2; pattern++) + { + int count = 0; + char **names = SDL_GlobDirectory(folder, patterns[pattern], 0, &count); + int name; + + for (name = 0; names && name < count; name++) + { + unsigned long size = 0; + unsigned char *data; + + if (file_find(names[name]) >= 0) + continue; + snprintf(path, sizeof(path), "%s/%s", folder, names[name]); + data = file_read(path, &size); + if (data) + { + platform_log("menus: adding %s", path); + file_add(names[name], data, size, 1); + } + } + SDL_free(names); + } + } +#endif +} + +/* a file's data (a bitmap's PNG, by the path its gives): the +menus folder's, else the embedded one */ +static const unsigned char *file_data(const char *path, unsigned long *size) +{ + long index = file_find(path); + + if (index < 0) + { + char full[1200]; + unsigned char *data; + + snprintf(full, sizeof(full), "%s/%s", folder, path); + data = file_read(full, size); + if (!data) + return NULL; + file_add(path, data, *size, 1); + index = file_find(path); + } + *size = files[index].size; + return files[index].data; +} + +/* ---------- reading */ + +enum element +{ + _element_none, + _element_menus, + _element_bitmap, + _element_frame, + _element_strings, + _element_string, + _element_widget, + _element_child, + _element_on, + _element_data, + _element_conditional, + _element_replace, +}; + +/* an attribute an element may have: its text (text) or number (number, +with given set when it is there) */ +struct attribute +{ + const char *name; + const char **text; + long *number; + long *given; +}; + +struct reader +{ + struct halo_menus menus; + const char *file; + XML_Parser parser; + int failed; + /* the open elements, and the widget, bitmap or strings each is in */ + enum element elements[MAXIMUM_DEPTH]; + long owners[MAXIMUM_DEPTH]; + int depth; + /* how deep inside an element for the other platform we are (0: not) */ + int skipping; + /* the open has frames= (its s cannot be added too) */ + int bitmap_frames_attribute; + /* each widget's last child, handler, input, conditional and replace, + for adding the next */ + long *last_child, *last_handler, *last_input, *last_conditional, *last_replace; +}; + +static void reader_error(struct reader *reader, const char *format, ...) +{ + char message[512]; + va_list arguments; + + va_start(arguments, format); + vsnprintf(message, sizeof(message), format, arguments); + va_end(arguments); + if (!reader->failed) + { + platform_log("menus: %s:%lu: %s", reader->file, + (unsigned long)XML_GetCurrentLineNumber(reader->parser), message); + } + reader->failed = 1; + XML_StopParser(reader->parser, XML_FALSE); +} + +static char *copy_length(const char *text, size_t length) +{ + char *result = malloc(length + 1); + + if (result) + { + memcpy(result, text, length); + result[length] = 0; + } + return result; +} + +static char *copy(const char *text) +{ + return text ? copy_length(text, strlen(text)) : NULL; +} + +static int parse_long(struct reader *reader, const char *name, const char *text, long *value) +{ + char *end; + + errno = 0; + *value = strtol(text, &end, 10); + if (!*text || *end) + { + reader_error(reader, "%s=\"%s\" is not a whole number", name, text); + return 0; + } + /* (the tags keep them in shorts) */ + if (errno == ERANGE || *value < -32768 || *value > 32767) + { + reader_error(reader, "%s=\"%s\" is out of range (-32768 to 32767)", name, text); + return 0; + } + return 1; +} + +/* the element's attributes into the table's fields; one not in the table +(but platform, which for_this_platform reads) is an error */ +static void read_attributes(struct reader *reader, const char *element, const XML_Char **attributes, + const struct attribute *table, size_t count) +{ + int index; + + for (index = 0; attributes[index] && !reader->failed; index += 2) + { + const char *name = attributes[index], *value = attributes[index + 1]; + size_t field; + + if (!strcmp(name, "platform")) + continue; + for (field = 0; field < count; field++) + { + if (strcmp(name, table[field].name)) + continue; + if (table[field].text) + *table[field].text = copy(value); + else if (parse_long(reader, name, value, table[field].number) && table[field].given) + *table[field].given = 1; + break; + } + if (field == count) + reader_error(reader, "<%s> has no attribute %s", element, name); + } +} + +static long current_line(struct reader *reader) +{ + return (long)XML_GetCurrentLineNumber(reader->parser); +} + +/* whether the element is for this platform (its platform attribute) */ +static int for_this_platform(struct reader *reader, const XML_Char **attributes) +{ + int index; + + for (index = 0; attributes[index]; index += 2) + { + if (!strcmp(attributes[index], "platform")) + { + const char *platform = attributes[index + 1]; + + if (strcmp(platform, "desktop") && strcmp(platform, "android")) + { + reader_error(reader, "platform=\"%s\" is not \"desktop\" or \"android\"", platform); + return 1; + } +#ifdef HALO_ANDROID + return !strcmp(platform, "android"); +#else + return !strcmp(platform, "desktop"); +#endif + } + } + return 1; +} + +/* room for one more element in an array of count */ +static void *grow(void *array, long count, size_t size) +{ + return realloc(array, (size_t)(count + 1) * size); +} + +static long *grow_last(long *array, long count) +{ + array = grow(array, count, sizeof(*array)); + array[count] = HALO_MENU_NONE; + return array; +} + +/* adds index to a widget's list (first, and each item's next) */ +#define LIST_ADD(reader, owner, first, last, items, index) \ + do \ + { \ + if ((reader)->last[owner] == HALO_MENU_NONE) \ + (reader)->menus.widgets[owner].first = (index); \ + else \ + (reader)->menus.items[(reader)->last[owner]].next = (index); \ + (reader)->last[owner] = (index); \ + } while (0) + +static void read_bitmap(struct reader *reader, const XML_Char **attributes) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_bitmap bitmap; + const char *frames = NULL; + long width = 0, height = 0; + struct attribute table[] = + { + { "name", &bitmap.name }, { "frames", &frames }, { "width", NULL, &width }, { "height", NULL, &height }, + }; + + memset(&bitmap, 0, sizeof(bitmap)); + bitmap.first_frame = menus->frame_count; + bitmap.file = reader->file; + bitmap.line = current_line(reader); + read_attributes(reader, "bitmap", attributes, table, sizeof(table) / sizeof(*table)); + reader->bitmap_frames_attribute = frames != NULL; + /* (frames="a.png b.png", all width by height, or s) */ + while (frames && *frames && !reader->failed) + { + size_t length; + + frames += strspn(frames, " \t\r\n"); + length = strcspn(frames, " \t\r\n"); + if (!length) + break; + menus->frames = grow(menus->frames, menus->frame_count, sizeof(*menus->frames)); + memset(&menus->frames[menus->frame_count], 0, sizeof(*menus->frames)); + menus->frames[menus->frame_count].png = copy_length(frames, length); + menus->frames[menus->frame_count].width = width; + menus->frames[menus->frame_count].height = height; + menus->frame_count++; + bitmap.frame_count++; + frames += length; + } + if (!reader->failed && !bitmap.name) + reader_error(reader, "a needs a name"); + menus->bitmaps = grow(menus->bitmaps, menus->bitmap_count, sizeof(*menus->bitmaps)); + menus->bitmaps[menus->bitmap_count++] = bitmap; +} + +static void read_frame(struct reader *reader, const XML_Char **attributes, long owner) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_frame frame; + struct attribute table[] = + { + { "png", &frame.png }, { "map", &frame.map }, { "index", NULL, &frame.index }, + { "width", NULL, &frame.width }, { "height", NULL, &frame.height }, + }; + + memset(&frame, 0, sizeof(frame)); + frame.index = -1; + read_attributes(reader, "frame", attributes, table, sizeof(table) / sizeof(*table)); + if (!reader->failed && (frame.png != NULL) == (frame.map != NULL)) + reader_error(reader, "a has a png or a map bitmap, not both"); + else if (!reader->failed && frame.png && (frame.width <= 0 || frame.height <= 0)) + reader_error(reader, "a with a png needs width and height"); + else if (!reader->failed && frame.map && frame.index < 0) + reader_error(reader, "a of a map bitmap needs its index"); + if (!reader->failed && reader->bitmap_frames_attribute) + reader_error(reader, "a bitmap's s and frames= cannot be mixed"); + if (frame.index < 0) + frame.index = 0; + menus->frames = grow(menus->frames, menus->frame_count, sizeof(*menus->frames)); + menus->frames[menus->frame_count++] = frame; + menus->bitmaps[owner].frame_count++; +} + +static void read_strings(struct reader *reader, const XML_Char **attributes) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_strings strings; + struct attribute table[] = { { "name", &strings.name } }; + + memset(&strings, 0, sizeof(strings)); + strings.first = menus->string_count; + strings.file = reader->file; + strings.line = current_line(reader); + read_attributes(reader, "strings", attributes, table, sizeof(table) / sizeof(*table)); + if (!reader->failed && !strings.name) + reader_error(reader, "a needs a name"); + menus->string_lists = grow(menus->string_lists, menus->string_list_count, sizeof(*menus->string_lists)); + menus->string_lists[menus->string_list_count++] = strings; +} + +static void read_string(struct reader *reader, const XML_Char **attributes, long owner) +{ + struct halo_menus *menus = &reader->menus; + const char *text = NULL; + struct attribute table[] = { { "text", &text } }; + + read_attributes(reader, "string", attributes, table, sizeof(table) / sizeof(*table)); + if (!reader->failed && !text) + reader_error(reader, "a needs text"); + menus->strings = grow(menus->strings, menus->string_count, sizeof(*menus->strings)); + menus->strings[menus->string_count++] = text ? text : ""; + menus->string_lists[owner].count++; +} + +static void child_add(struct reader *reader, long parent, long nested, const char *widget, long x, long y, + const char *controller) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_child child; + long index = menus->child_count; + + memset(&child, 0, sizeof(child)); + child.nested = nested; + child.widget = widget; + child.x = x; + child.y = y; + child.controller = controller; + child.next = HALO_MENU_NONE; + child.file = reader->file; + child.line = current_line(reader); + menus->children = grow(menus->children, menus->child_count, sizeof(*menus->children)); + menus->children[menus->child_count++] = child; + LIST_ADD(reader, parent, first_child, last_child, children, index); +} + +static void read_widget(struct reader *reader, const XML_Char **attributes, long parent) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_widget widget; + long index = menus->widget_count; + const char *child_controller = NULL; + struct attribute table[] = + { + { "name", &widget.name }, { "type", &widget.type }, { "controller", &widget.controller }, + { "flags", &widget.flags }, { "bitmap", &widget.bitmap }, { "text", &widget.text }, + { "strings", &widget.strings }, { "values", &widget.values }, { "setting", &widget.setting }, + { "font", &widget.font }, { "color", &widget.color }, { "align", &widget.align }, + { "description", &widget.description }, { "string_list", &widget.string_list }, + { "list_flags", &widget.list_flags }, { "text_flags", &widget.text_flags }, + { "header_bitmap", &widget.header_bitmap }, { "footer_bitmap", &widget.footer_bitmap }, + { "header_bounds", &widget.header_bounds }, { "footer_bounds", &widget.footer_bounds }, + { "child_controller", &child_controller }, + { "x", NULL, &widget.x }, { "y", NULL, &widget.y }, { "left", NULL, &widget.left }, + { "top", NULL, &widget.top }, { "width", NULL, &widget.width, &widget.has_width }, + { "height", NULL, &widget.height, &widget.has_height }, { "text_x", NULL, &widget.text_x }, + { "text_y", NULL, &widget.text_y }, { "auto_close", NULL, &widget.auto_close }, + { "auto_close_fade", NULL, &widget.auto_close_fade }, { "string_index", NULL, &widget.string_index }, + }; + + memset(&widget, 0, sizeof(widget)); + widget.parent = parent; + widget.first_child = widget.first_handler = widget.first_input = HALO_MENU_NONE; + widget.first_conditional = widget.first_replace = HALO_MENU_NONE; + widget.file = reader->file; + widget.line = current_line(reader); + read_attributes(reader, "widget", attributes, table, sizeof(table) / sizeof(*table)); + if (!reader->failed && !widget.name) + reader_error(reader, "a needs a name"); + menus->widgets = grow(menus->widgets, index, sizeof(*menus->widgets)); + menus->widgets[index] = widget; + menus->widget_count++; + reader->last_child = grow_last(reader->last_child, index); + reader->last_handler = grow_last(reader->last_handler, index); + reader->last_input = grow_last(reader->last_input, index); + reader->last_conditional = grow_last(reader->last_conditional, index); + reader->last_replace = grow_last(reader->last_replace, index); + if (parent != HALO_MENU_NONE) + child_add(reader, parent, index, NULL, widget.x, widget.y, child_controller); +} + +static void read_child(struct reader *reader, const XML_Char **attributes, long owner) +{ + const char *widget = NULL, *controller = NULL; + long x = 0, y = 0; + struct attribute table[] = + { + { "widget", &widget }, { "controller", &controller }, { "x", NULL, &x }, { "y", NULL, &y }, + }; + + read_attributes(reader, "child", attributes, table, sizeof(table) / sizeof(*table)); + if (!reader->failed && !widget) + reader_error(reader, "a needs a widget"); + child_add(reader, owner, HALO_MENU_NONE, widget, x, y, controller); +} + +static void read_handler(struct reader *reader, const XML_Char **attributes, long owner) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_handler handler; + long index = menus->handler_count; + const char *back = NULL, *branch = NULL; + struct attribute table[] = + { + { "event", &handler.event }, { "run", &handler.run }, { "script", &handler.script }, + { "open", &handler.open }, { "replace", &handler.replace }, { "close", &handler.close }, + { "widget", &handler.widget }, { "focus", &handler.focus }, { "reload", &handler.reload }, + { "sound", &handler.sound }, { "otherwise", &handler.otherwise }, { "label", &handler.label }, + { "back", &back }, { "branch", &branch }, + }; + + memset(&handler, 0, sizeof(handler)); + handler.next = HALO_MENU_NONE; + handler.file = reader->file; + handler.line = current_line(reader); + read_attributes(reader, "on", attributes, table, sizeof(table) / sizeof(*table)); + if ((back && strcmp(back, "true") && strcmp(back, "false")) || + (branch && strcmp(branch, "true") && strcmp(branch, "false"))) + reader_error(reader, "back and branch are \"true\" or \"false\""); + handler.back = back && !strcmp(back, "true"); + handler.branch = branch && !strcmp(branch, "true"); + if (!reader->failed && !handler.event) + reader_error(reader, "an needs an event"); + menus->handlers = grow(menus->handlers, index, sizeof(*menus->handlers)); + menus->handlers[index] = handler; + menus->handler_count++; + LIST_ADD(reader, owner, first_handler, last_handler, handlers, index); +} + +static void read_input(struct reader *reader, const XML_Char **attributes, long owner) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_input input; + long index = menus->input_count; + struct attribute table[] = { { "input", &input.input } }; + + memset(&input, 0, sizeof(input)); + input.next = HALO_MENU_NONE; + input.file = reader->file; + input.line = current_line(reader); + read_attributes(reader, "data", attributes, table, sizeof(table) / sizeof(*table)); + if (!reader->failed && !input.input) + reader_error(reader, "a needs an input"); + menus->inputs = grow(menus->inputs, index, sizeof(*menus->inputs)); + menus->inputs[index] = input; + menus->input_count++; + LIST_ADD(reader, owner, first_input, last_input, inputs, index); +} + +static void read_conditional(struct reader *reader, const XML_Char **attributes, long owner) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_conditional conditional; + long index = menus->conditional_count; + const char *if_failed = NULL; + struct attribute table[] = { { "widget", &conditional.widget }, { "if_failed", &if_failed } }; + + memset(&conditional, 0, sizeof(conditional)); + conditional.next = HALO_MENU_NONE; + conditional.file = reader->file; + conditional.line = current_line(reader); + read_attributes(reader, "conditional", attributes, table, sizeof(table) / sizeof(*table)); + if (if_failed && strcmp(if_failed, "true") && strcmp(if_failed, "false")) + reader_error(reader, "if_failed is \"true\" or \"false\""); + conditional.if_failed = if_failed && !strcmp(if_failed, "true"); + if (!reader->failed && !conditional.widget) + reader_error(reader, "a needs a widget"); + menus->conditionals = grow(menus->conditionals, index, sizeof(*menus->conditionals)); + menus->conditionals[index] = conditional; + menus->conditional_count++; + LIST_ADD(reader, owner, first_conditional, last_conditional, conditionals, index); +} + +static void read_replace(struct reader *reader, const XML_Char **attributes, long owner) +{ + struct halo_menus *menus = &reader->menus; + struct halo_menu_replace replace; + long index = menus->replace_count; + struct attribute table[] = { { "search", &replace.search }, { "function", &replace.function } }; + + memset(&replace, 0, sizeof(replace)); + replace.next = HALO_MENU_NONE; + replace.file = reader->file; + replace.line = current_line(reader); + read_attributes(reader, "replace", attributes, table, sizeof(table) / sizeof(*table)); + if (!reader->failed && !replace.search) + reader_error(reader, "a needs a search"); + menus->replaces = grow(menus->replaces, index, sizeof(*menus->replaces)); + menus->replaces[index] = replace; + menus->replace_count++; + LIST_ADD(reader, owner, first_replace, last_replace, replaces, index); +} + +static void read_menus(struct reader *reader, const XML_Char **attributes) +{ + const char *root = NULL; + struct attribute table[] = { { "root", &root } }; + + read_attributes(reader, "menus", attributes, table, sizeof(table) / sizeof(*table)); + if (root) + reader->menus.root = root; +} + +static void XMLCALL element_start(void *data, const XML_Char *name, const XML_Char **attributes) +{ + struct reader *reader = data; + enum element parent = reader->depth ? reader->elements[reader->depth - 1] : _element_none; + long owner = reader->depth ? reader->owners[reader->depth - 1] : HALO_MENU_NONE; + enum element element = _element_none; + + if (reader->failed) + return; + if (reader->skipping || !for_this_platform(reader, attributes)) + { + reader->skipping++; + return; + } + if (reader->depth == MAXIMUM_DEPTH) + { + reader_error(reader, "elements nested too deeply"); + return; + } + if (!strcmp(name, "menus") && parent == _element_none) + { + element = _element_menus; + read_menus(reader, attributes); + } + else if (!strcmp(name, "bitmap") && parent == _element_menus) + { + element = _element_bitmap; + read_bitmap(reader, attributes); + owner = reader->menus.bitmap_count - 1; + } + else if (!strcmp(name, "frame") && parent == _element_bitmap) + { + element = _element_frame; + read_frame(reader, attributes, owner); + } + else if (!strcmp(name, "strings") && parent == _element_menus) + { + element = _element_strings; + read_strings(reader, attributes); + owner = reader->menus.string_list_count - 1; + } + else if (!strcmp(name, "string") && parent == _element_strings) + { + element = _element_string; + read_string(reader, attributes, owner); + } + else if (!strcmp(name, "widget") && (parent == _element_menus || parent == _element_widget)) + { + element = _element_widget; + read_widget(reader, attributes, parent == _element_widget ? owner : HALO_MENU_NONE); + owner = reader->menus.widget_count - 1; + } + else if (parent == _element_widget) + { + if (!strcmp(name, "child")) + { + element = _element_child; + read_child(reader, attributes, owner); + } + else if (!strcmp(name, "on")) + { + element = _element_on; + read_handler(reader, attributes, owner); + } + else if (!strcmp(name, "data")) + { + element = _element_data; + read_input(reader, attributes, owner); + } + else if (!strcmp(name, "conditional")) + { + element = _element_conditional; + read_conditional(reader, attributes, owner); + } + else if (!strcmp(name, "replace")) + { + element = _element_replace; + read_replace(reader, attributes, owner); + } + } + if (element == _element_none) + { + reader_error(reader, "<%s> cannot be here", name); + return; + } + reader->elements[reader->depth] = element; + reader->owners[reader->depth] = owner; + reader->depth++; +} + +static void XMLCALL element_end(void *data, const XML_Char *name) +{ + struct reader *reader = data; + + (void)name; + if (reader->skipping) + reader->skipping--; + else if (reader->depth) + reader->depth--; +} + +static void XMLCALL text(void *data, const XML_Char *characters, int length) +{ + struct reader *reader = data; + int index; + + if (reader->failed || reader->skipping) + return; + for (index = 0; index < length; index++) + { + if (!strchr(" \t\r\n", characters[index])) + { + reader_error(reader, "text belongs in an attribute (text=\"...\")"); + return; + } + } +} + +static int read_file(struct reader *reader, const struct menu_file *file) +{ + reader->file = file->path; + reader->depth = 0; + reader->skipping = 0; + reader->parser = XML_ParserCreate("UTF-8"); + if (!reader->parser) + { + platform_log("menus: %s: no XML parser (out of memory)", file->path); + reader->failed = 1; + return 0; + } + XML_SetUserData(reader->parser, reader); + XML_SetElementHandler(reader->parser, element_start, element_end); + XML_SetCharacterDataHandler(reader->parser, text); + if (XML_Parse(reader->parser, (const char *)file->data, (int)file->size, XML_TRUE) == XML_STATUS_ERROR && + !reader->failed) + { + platform_log("menus: %s:%lu:%lu: %s", file->path, (unsigned long)XML_GetCurrentLineNumber(reader->parser), + (unsigned long)XML_GetCurrentColumnNumber(reader->parser), + XML_ErrorString(XML_GetErrorCode(reader->parser))); + reader->failed = 1; + } + XML_ParserFree(reader->parser); + return !reader->failed; +} + +/* ---------- public code */ + +struct halo_menus const *halo_menus_load(void) +{ + static int read; + static struct halo_menus menus; + static int succeeded; + struct reader reader; + long index; + + if (read) + return succeeded ? &menus : NULL; + read = 1; + files_gather(); + memset(&reader, 0, sizeof(reader)); + for (index = 0; index < file_count && !reader.failed; index++) + { + size_t length = strlen(files[index].path); + + if (length > 4 && !strcmp(files[index].path + length - 4, ".xml")) + read_file(&reader, &files[index]); + } + free(reader.last_child); + free(reader.last_handler); + free(reader.last_input); + free(reader.last_conditional); + free(reader.last_replace); + if (reader.failed || !reader.menus.widget_count) + { + if (reader.failed) + platform_log("menus: the files have a problem; using the game's own menus"); + return NULL; + } + if (!reader.menus.root) + reader.menus.root = "main_menu"; + menus = reader.menus; + succeeded = 1; + return &menus; +} + +long halo_menus_utf16(char const *utf8, unsigned short *out, long capacity) +{ + const unsigned char *in = (const unsigned char *)utf8; + long count = 0; + + while (*in && count < capacity - 1) + { + unsigned long code = *in++; + + if (code == '\\' && *in == 'n') + { + in++; + if (count + 2 >= capacity) + break; + out[count++] = '\r'; + out[count++] = '\n'; + continue; + } + if (code >= 0xC0) + { + int more = code >= 0xF0 ? 3 : code >= 0xE0 ? 2 : 1; + + code &= 0x3F >> more; + while (more-- && (*in & 0xC0) == 0x80) + code = (code << 6) | (*in++ & 0x3F); + } + /* (beyond the 16-bit characters: the game's fonts have none) */ + out[count++] = code < 0x10000 ? (unsigned short)code : '?'; + } + out[count++] = 0; + return count; +} + +void halo_menus_log(char const *file, long line, char const *message, char const *detail) +{ + platform_log("menus: %s:%ld: %s%s%s", file ? file : "?", line, message, detail ? " " : "", detail ? detail : ""); +} + +/* ---------- art */ + +static struct +{ + unsigned long data; + char *png; + unsigned int texture; + unsigned long levels; + int failed; +} art[MAXIMUM_ART]; +static long art_count; + +void halo_menus_art_register(void const *texture, char const *png) +{ + unsigned long data = ((const unsigned long *)texture)[1]; + long index; + + for (index = 0; index < art_count; index++) + { + if (art[index].data == data) + break; + } + if (index == art_count) + { + if (art_count == MAXIMUM_ART) + { + platform_log("menus: more than %d bitmaps; %s is not drawn", MAXIMUM_ART, png); + return; + } + art_count++; + } + else if (art[index].png && !strcmp(art[index].png, png)) + { + return; + } + else + { + free(art[index].png); + if (art[index].texture) + glDeleteTextures(1, &art[index].texture); + } + memset(&art[index], 0, sizeof(art[index])); + art[index].data = data; + art[index].png = strdup(png); +} + +void halo_menus_art_forget(void) +{ + long index; + + for (index = 0; index < art_count; index++) + { + free(art[index].png); + if (art[index].texture) + glDeleteTextures(1, &art[index].texture); + } + art_count = 0; +} + +unsigned int menu_art_texture(unsigned long data, unsigned long *levels) +{ + long index; + + for (index = 0; index < art_count; index++) + { + if (art[index].data == data) + break; + } + if (index == art_count || art[index].failed) + return 0; + if (!art[index].texture) + { + unsigned long size = 0; + const unsigned char *png = file_data(art[index].png, &size); + + art[index].texture = png ? hud_hires_png_texture(png, size, &art[index].levels) : 0; + if (!art[index].texture) + { + platform_log("menus: could not draw %s", art[index].png); + art[index].failed = 1; + return 0; + } + } + *levels = art[index].levels; + return art[index].texture; +} diff --git a/port/linux/src/menu_files.h b/port/linux/src/menu_files.h new file mode 100644 index 00000000..ac809680 --- /dev/null +++ b/port/linux/src/menu_files.h @@ -0,0 +1,27 @@ +/* +MENU_FILES.H + +The menus' files (port/assets/menus: their XML, and the PNGs of their +bitmaps), embedded by tools/embed_assets.py; menu_files.c reads them, and +those in a menus folder beside config.toml, for the game (halo_menus.h). +*/ + +#ifndef MENU_FILES_H +#define MENU_FILES_H + +/* an embedded file, by its path in port/assets/menus */ +struct menu_file_embedded +{ + const char *path; + const unsigned int *data; + unsigned int size; +}; + +extern const struct menu_file_embedded menu_files_embedded[]; +extern const unsigned int menu_files_embedded_count; + +/* the GL texture standing for a menu bitmap's D3D texture (data: its Data), +decoded from its PNG on first use, and its mip levels; 0 if it is none */ +unsigned int menu_art_texture(unsigned long data, unsigned long *levels); + +#endif diff --git a/port/linux/src/p2p.c b/port/linux/src/p2p.c index cbecc337..cf9382dc 100644 --- a/port/linux/src/p2p.c +++ b/port/linux/src/p2p.c @@ -309,9 +309,11 @@ static struct int stun_started; int reported_symmetric; - /* hosting: while the game listens on hosting_socket */ + /* hosting: while the game listens on hosting_socket, and its game may be + joined from the internet (p2p_set_hosting_allowed) */ int hosting_socket; int hosting; + int hosting_lan_only; int has_token; unsigned char token[P2P_TOKEN_SIZE]; char invite[P2P_LINK_SIZE]; @@ -2359,7 +2361,7 @@ static int connected_player_count(void) static void update_hosting(void) { - int want = p2p.hosting_socket >= 0; + int want = p2p.hosting_socket >= 0 && !p2p.hosting_lan_only; if (want && !p2p.hosting) { @@ -2414,6 +2416,24 @@ static void update_hosting(void) } } +void p2p_set_hosting_allowed(int allowed) +{ + pthread_mutex_lock(&p2p_lock); + p2p.hosting_lan_only = !allowed; + pthread_mutex_unlock(&p2p_lock); +} + +int p2p_invite_link(char *link, int size) +{ + int hosting; + + pthread_mutex_lock(&p2p_lock); + hosting = p2p.hosting && p2p.has_token; + snprintf(link, (size_t)size, "%s", hosting ? p2p.invite : ""); + pthread_mutex_unlock(&p2p_lock); + return hosting; +} + void p2p_set_game_player_counts(int count, int maximum) { /* (only the game's server writes these: unchanged, it need not wait diff --git a/port/linux/src/p2p.h b/port/linux/src/p2p.h index d423532a..c8c6ccb4 100644 --- a/port/linux/src/p2p.h +++ b/port/linux/src/p2p.h @@ -71,6 +71,14 @@ void p2p_socket_closed(int socket, unsigned short datagram_port); from the main thread */ const char *p2p_take_clipboard_text(void); +/* whether a game this machine hosts may be joined from the internet (an +invite, Discord): the PC menus' Create Game > Internet (the default), not +> LAN */ +void p2p_set_hosting_allowed(int allowed); +/* the invite link of the game this machine hosts (empty, 0: none, or LAN +only) */ +int p2p_invite_link(char *link, int size); + /* the hosted game's players and the most it takes, which Discord shows (0, 0: not hosting; until the game says, the machines the tunnel reaches are shown). The game's server calls it as they change (calling it with the diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 88b0048a..f03e37d6 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -68,6 +68,11 @@ static const struct config_setting config_settings[] = { "display.fullscreen", _config_boolean, "true", "HALO_FULLSCREEN", _environment_value, _platform_desktop, "Start fullscreen, drawing at the display's resolution and shape; false\n" "starts in a window, which draws the Xbox's 640x480. F11 switches." }, + { "display.mode", _config_string, "\"\"", "HALO_DISPLAY_MODE", _environment_value, _platform_desktop, + "\"fullscreen\" takes the display at its desktop resolution, \"borderless\"\n" + "is a window over the whole desktop (both draw at the display's\n" + "resolution), \"windowed\" a window of the Xbox's 640x480, scaled. Empty:\n" + "display.fullscreen's (true: borderless). F11 switches to the window and back." }, { "display.window_scale", _config_integer, "2", "HALO_WINDOW_SCALE", _environment_value, _platform_desktop, "The window's size as a multiple of 640x480 (it can be resized)." }, { "display.screen_width", _config_integer, "0", "HALO_SCREEN_WIDTH", _environment_value, _platform_android, @@ -93,6 +98,9 @@ static const struct config_setting config_settings[] = "Draw the menus' and HUD's text with the fonts in port/assets/fonts\n" "(Overpass) at the display's resolution, and the menus' titles from\n" "port/assets/titles; false draws the maps' bitmap fonts and titles." }, + { "display.menus", _config_string, "\"pc\"", "HALO_MENUS", _environment_value, _platform_all, + "The menus: \"pc\" for the PC version's main menu (port/assets/menus,\n" + "and a menus folder here for your own), \"xbox\" for the Xbox's." }, { "display.player_names", _config_string, "\"all\"", "HALO_PLAYER_NAMES", _environment_value, _platform_all, "In multiplayer, whose names are drawn above their heads: \"all\",\n" "\"allies\", \"enemies\" or \"none\". An enemy's shows only while in sight\n" @@ -117,6 +125,11 @@ static const struct config_setting config_settings[] = "Play sound." }, { "audio.volume", _config_real, "1.0", "HALO_VOLUME", _environment_value, _platform_all, "The volume of everything, 0.0 to 1.0." }, + { "audio.music_volume", _config_real, "1.0", "HALO_MUSIC_VOLUME", _environment_value, _platform_all, + "The music's volume, 0.0 to 1.0 (of audio.volume)." }, + { "audio.effects_volume", _config_real, "1.0", "HALO_EFFECTS_VOLUME", _environment_value, _platform_all, + "The volume of every other sound (effects and speech), 0.0 to 1.0 (of\n" + "audio.volume)." }, { "input.mouse_sensitivity", _config_real, "1.0", "HALO_MOUSE_SENSITIVITY", _environment_value, _platform_desktop, "How far the view turns for the mouse's movement." }, @@ -127,6 +140,54 @@ static const struct config_setting config_settings[] = "slowed and dragged along by a target. The last of the mouse and the\n" "right stick to move decides. The bullets' autoaim (bent toward the\n" "target) stays either way." }, + { "input.mouse_vertical_sensitivity", _config_real, "0.0", "HALO_MOUSE_VERTICAL_SENSITIVITY", _environment_value, + _platform_desktop, + "How far the view turns up and down for the mouse's movement; 0 for the\n" + "same as input.mouse_sensitivity." }, + + /* the keyboard and mouse's own controls (port/linux/src/xinput_sdl.c) */ + { "controls.move_forward", _config_string, "\"W\"", "HALO_KEY_MOVE_FORWARD", _environment_value, _platform_all, + "The keyboard and mouse's controls, which Settings > Controls Setup\n" + "changes: up to two keys or buttons each, separated by a comma. Keys by\n" + "their names (\"W\", \"Space\", \"Left Ctrl\", \"F1\"), and \"Mouse Left\",\n" + "\"Mouse Right\", \"Mouse Middle\", \"Mouse 4\", \"Mouse 5\", \"Wheel\" (either\n" + "way), \"Wheel Up\" and \"Wheel Down\"; empty for none. Moving forward:" }, + { "controls.move_backward", _config_string, "\"S\"", "HALO_KEY_MOVE_BACKWARD", _environment_value, _platform_all, + "Moving backward." }, + { "controls.strafe_left", _config_string, "\"A\"", "HALO_KEY_STRAFE_LEFT", _environment_value, _platform_all, + "Moving left." }, + { "controls.strafe_right", _config_string, "\"D\"", "HALO_KEY_STRAFE_RIGHT", _environment_value, _platform_all, + "Moving right." }, + { "controls.jump", _config_string, "\"Space\"", "HALO_KEY_JUMP", _environment_value, _platform_all, + "Jumping (and skipping cutscenes)." }, + { "controls.crouch", _config_string, "\"Left Ctrl, C\"", "HALO_KEY_CROUCH", _environment_value, _platform_all, + "Crouching." }, + { "controls.fire", _config_string, "\"Mouse Left\"", "HALO_KEY_FIRE", _environment_value, _platform_all, + "Firing." }, + { "controls.throw_grenade", _config_string, "\"Mouse Right, G\"", "HALO_KEY_THROW_GRENADE", _environment_value, + _platform_all, + "Throwing a grenade." }, + { "controls.melee", _config_string, "\"F, Mouse 4\"", "HALO_KEY_MELEE", _environment_value, _platform_all, + "Melee attack." }, + { "controls.reload", _config_string, "\"R\"", "HALO_KEY_RELOAD", _environment_value, _platform_all, + "Reloading." }, + { "controls.zoom", _config_string, "\"Z, Mouse Middle\"", "HALO_KEY_ZOOM", _environment_value, _platform_all, + "Zooming the scope." }, + { "controls.switch_weapon", _config_string, "\"Wheel, 1\"", "HALO_KEY_SWITCH_WEAPON", _environment_value, + _platform_all, + "Switching weapons." }, + { "controls.switch_grenade", _config_string, "\"X\"", "HALO_KEY_SWITCH_GRENADE", _environment_value, _platform_all, + "Switching grenades." }, + { "controls.action", _config_string, "\"E\"", "HALO_KEY_ACTION", _environment_value, _platform_all, + "The action: picking up (held: swapping weapons), entering and leaving\n" + "vehicles, pressing switches; never reloading (the controller's X does\n" + "when there is nothing to act on)." }, + { "controls.flashlight", _config_string, "\"Q\"", "HALO_KEY_FLASHLIGHT", _environment_value, _platform_all, + "The flashlight." }, + { "controls.scoreboard", _config_string, "\"Tab\"", "HALO_KEY_SCOREBOARD", _environment_value, _platform_all, + "Showing the scores (the controller's Back)." }, + { "controls.pause", _config_string, "\"Escape\"", "HALO_KEY_PAUSE", _environment_value, _platform_all, + "The pause menu (the controller's Start)." }, { "game.console_log", _config_string, "\"important\"", "HALO_CONSOLE_LOG", _environment_value, _platform_all, "What the game's console shows on screen of what it logs: \"important\"\n" @@ -242,6 +303,9 @@ static const struct config_setting config_settings[] = "Run without a window, drawing nothing." }, { "debug.gl_debug", _config_boolean, "false", "HALO_GL_DEBUG", _environment_set_is_true, _platform_all, "Report OpenGL errors in the log." }, + { "debug.menu_open", _config_string, "\"\"", "HALO_MENU_OPEN", _environment_value, _platform_all, + "Start on this screen of the menus (port/assets/menus) instead of the main\n" + "menu, a player profile being edited; empty for the main menu." }, { "debug.gpu_flush_draws", _config_integer, "-1", "HALO_GPU_FLUSH_DRAWS", _environment_value, _platform_desktop, "Flush the GPU's pipeline every this many draws: -1 for every 3 on Intel\n" "graphics with Mesa's driver (which can hang without), 0 never." }, @@ -596,7 +660,8 @@ static void config_set_from_text(struct config_value *value, enum config_type ty value->real = strtod(text, NULL); break; case _config_string: - free(value->string); + /* (the old string is kept, not freed: config_string's callers hold + its pointer, and Settings writes few, seldom) */ value->string = strdup(text); break; } @@ -654,6 +719,10 @@ static void config_set_from_file(struct config_value *value, const struct config } } +/* how many times a setting has been written (config_write): readers that +keep a setting watch this, to read it again */ +static volatile unsigned long config_change_count; + static long config_setting_index(const char *name) { size_t index; @@ -830,28 +899,58 @@ static int config_line_section(const char *line, const char *end, char *section, return 1; } -/* sets a boolean setting, for now and in config.toml: its line there is -changed (or added), the rest of the file kept as it is */ -int config_write_boolean(const char *name, int value) +/* sets a setting, for now and in config.toml, from its value as text +("true", "60", "1.5", "all"): its line there is changed (or added), the rest +of the file kept as it is */ +int config_write(const char *name, const char *value) { const char *dot = strchr(name, '.'); long index = config_setting_index(name); - char section[64], key[64], wanted[80], current[64] = "", line_text[96], path[1024]; + char section[64], key[64], wanted[80], current[64] = "", line_text[600], path[1024]; struct config_text out = { 0 }; size_t size = 0; char *text; const char *line; int written = 0, in_section = 0, succeeded; - if (index < 0 || config_settings[index].type != _config_boolean || !dot || (size_t)(dot - name) >= sizeof(section)) + if (index < 0 || !dot || (size_t)(dot - name) >= sizeof(section) || strlen(value) > 256) return 0; /* (the file read first, as the other settings are) */ - config_boolean(name); + config_value(name, config_settings[index].type); pthread_mutex_lock(&config_lock); - config_values[index].boolean = value != 0; + config_set_from_text(&config_values[index], config_settings[index].type, value); snprintf(section, sizeof(section), "%.*s", (int)(dot - name), name); snprintf(key, sizeof(key), "%s", dot + 1); - snprintf(line_text, sizeof(line_text), "%s = %s\n", key, value ? "true" : "false"); + switch (config_settings[index].type) + { + case _config_boolean: + snprintf(line_text, sizeof(line_text), "%s = %s\n", key, config_values[index].boolean ? "true" : "false"); + break; + case _config_integer: + snprintf(line_text, sizeof(line_text), "%s = %ld\n", key, config_values[index].integer); + break; + case _config_real: + /* (with its point: TOML reads 1 as an integer) */ + snprintf(line_text, sizeof(line_text), "%s = %.15g", key, config_values[index].real); + if (!strpbrk(line_text + strlen(key) + 3, ".en")) + strcat(line_text, ".0"); + strcat(line_text, "\n"); + break; + case _config_string: + { + char *end = line_text + snprintf(line_text, sizeof(line_text), "%s = \"", key); + const char *character; + + for (character = config_values[index].string; *character; character++) + { + if (*character == '"' || *character == '\\') + *end++ = '\\'; + *end++ = *character; + } + strcpy(end, "\"\n"); + break; + } + } snprintf(wanted, sizeof(wanted), "%s", section); config_path(path, sizeof(path)); text = config_read_file(path, &size); @@ -902,14 +1001,93 @@ int config_write_boolean(const char *name, int value) config_append(&out, line_text); } succeeded = out.buffer && config_write_file(path, out.buffer); + config_change_count++; pthread_mutex_unlock(&config_lock); free(out.buffer); free(text); return succeeded; } +int config_write_boolean(const char *name, int value) +{ + long index = config_setting_index(name); + + return index >= 0 && config_settings[index].type == _config_boolean && config_write(name, value ? "true" : "false"); +} + +/* a setting's value as text ("true", "60", "1.5", "all"): 0 if there is no +such setting */ +int config_text(const char *name, char *text, size_t size) +{ + long index = config_setting_index(name); + const struct config_value *value; + + if (index < 0) + return 0; + value = config_value(name, config_settings[index].type); + switch (config_settings[index].type) + { + case _config_boolean: + snprintf(text, size, "%s", value->boolean ? "true" : "false"); + break; + case _config_integer: + snprintf(text, size, "%ld", value->integer); + break; + case _config_real: + snprintf(text, size, "%.15g", value->real); + break; + case _config_string: + snprintf(text, size, "%s", value->string ? value->string : ""); + break; + } + return 1; +} + +void config_folder(char *path, size_t size) +{ + char file[1024]; + char *separator; + + config_path(file, sizeof(file)); + separator = strrchr(file, '/'); +#ifndef HALO_ANDROID + if (!separator || (strrchr(file, '\\') && strrchr(file, '\\') > separator)) + separator = strrchr(file, '\\'); +#endif + if (separator) + separator[1] = 0; + else + file[0] = 0; + snprintf(path, size, "%s", file); +} + /* ---------- public code */ +unsigned long config_changes(void) +{ + return config_change_count; +} + +int config_default(const char *name, char *text, size_t size) +{ + long index = config_setting_index(name); + const char *value; + size_t length; + + if (index < 0) + return 0; + value = config_settings[index].default_value; + length = strlen(value); + /* (a string's without its quotes: the defaults have no escapes) */ + if (config_settings[index].type == _config_string && length >= 2 && value[0] == '"') + { + value++; + length -= 2; + } + snprintf(text, size, "%.*s", (int)length, value); + return 1; +} + int config_boolean(const char *name) { return config_value(name, _config_boolean)->boolean; diff --git a/port/linux/src/port_config.h b/port/linux/src/port_config.h index 9b66ec54..14213a5d 100644 --- a/port/linux/src/port_config.h +++ b/port/linux/src/port_config.h @@ -13,13 +13,26 @@ Settings are named "section.key", as in the file: "display.vsync". #ifndef PORT_CONFIG_H #define PORT_CONFIG_H +#include + int config_boolean(const char *name); long config_integer(const char *name); double config_real(const char *name); /* never NULL; "" when unset */ const char *config_string(const char *name); -/* sets a boolean setting, and writes it into config.toml (only its line -changes); 1 on success */ +/* sets a setting from its value as text ("true", "60", "1.5", "all"), and +writes it into config.toml (only its line changes); 1 on success */ +int config_write(const char *name, const char *value); int config_write_boolean(const char *name, int value); +/* a setting's value as text ("true", "60", "1.5", "all"); 0 if there is no +such setting */ +int config_text(const char *name, char *text, size_t size); +/* the folder config.toml is in, with its separator */ +void config_folder(char *path, size_t size); +/* a setting's default, as text; 0 if there is no such setting */ +int config_default(const char *name, char *text, size_t size); +/* how many times config_write has changed a setting: what keeps one reads +it again when this moves */ +unsigned long config_changes(void); #endif diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 84ef2ef2..e7f7e102 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -33,12 +33,36 @@ static struct platform_input_state input_state; /* keys pressed since the last read, so a press and release between two reads still counts as a press (input injected on Android, or a slow frame) */ static unsigned char keys_pressed[SDL_SCANCODE_COUNT]; +/* likewise the mouse buttons pressed since the last read, so that a click +quicker than a frame still counts */ +static unsigned char mouse_buttons_pressed[PLATFORM_MOUSE_BUTTON_COUNT]; #ifndef HALO_ANDROID /* the menus' pointer (platform_ui_pointer_set_active), under input_lock */ static struct platform_ui_pointer ui_pointer; static float ui_pointer_wheel; #endif static pthread_mutex_t input_lock = PTHREAD_MUTEX_INITIALIZER; +/* rebinding (platform_binding_capture_begin), under input_lock: waiting for +an input, and the one taken; then the keyboard and mouse settle (their keys +and buttons held then are let go) before they drive anything again */ +enum +{ + _binding_capture_idle, + _binding_capture_waiting, + _binding_capture_taken, +}; +static int binding_capture; +static int binding_capture_result; +static int binding_captured_input; +static BOOL binding_settling; +static Uint64 binding_taken_ms; +/* when the menus last asked (a capture they stop asking about, their screen +gone, ends: else the keyboard stays held from the game) */ +static Uint64 binding_polled_ms; +#define BINDING_ABANDONED_MS 500 +static unsigned mouse_buttons_down; +/* (an input taken that nothing asks for is let go after this) */ +#define BINDING_UNCLAIMED_MS 2000 /* the multiplayer scoreboard is open (platform_scoreboard_scroll): the wheel and Page Up/Down scroll it, and the wheel switches no weapon; how far they have moved it since the game last asked (notches down, pages down). Open @@ -313,19 +337,63 @@ BOOL platform_offer_game_data(const char *destination) int halo_interpolation_enabled(void) { - static int enabled = -1; + static int enabled; + static unsigned long read_at = (unsigned long)-1; - if (enabled < 0) + if (read_at != config_changes()) + { + read_at = config_changes(); enabled = config_boolean("display.interpolation"); + } return enabled; } #ifndef HALO_ANDROID -/* whether the window opens fullscreen (display.fullscreen), never when it -is hidden */ +/* the display mode (display.mode, else display.fullscreen's: borderless or +the window) */ +enum +{ + _display_mode_windowed = 0, + _display_mode_borderless, + _display_mode_fullscreen +}; + +static int platform_display_mode(void) +{ + const char *mode = config_string("display.mode"); + + if (!strcmp(mode, "fullscreen")) + return _display_mode_fullscreen; + if (!strcmp(mode, "borderless")) + return _display_mode_borderless; + if (!strcmp(mode, "windowed")) + return _display_mode_windowed; + return config_boolean("display.fullscreen") ? _display_mode_borderless : _display_mode_windowed; +} + +/* whether the window opens fullscreen (either kind), never when it is +hidden */ static BOOL platform_fullscreen_setting(void) { - return !config_boolean("debug.hidden_window") && config_boolean("display.fullscreen"); + return !config_boolean("debug.hidden_window") && platform_display_mode() != _display_mode_windowed; +} + +/* the window's fullscreen kind (display.mode): borderless, a window over +the whole desktop (SDL's fullscreen without a mode), or fullscreen, the +display taken at its desktop resolution. Either draws at the display's +resolution (platform_screen_mode); F11 switches to the kind set. */ +static void platform_fullscreen_kind_apply(void) +{ + static int applied = -1; + int exclusive = platform_display_mode() == _display_mode_fullscreen ? 1 : 0; + SDL_DisplayID display; + + if (!platform_window || exclusive == applied) + return; + applied = exclusive; + display = SDL_GetDisplayForWindow(platform_window); + SDL_SetWindowFullscreenMode(platform_window, + exclusive && display ? SDL_GetDesktopDisplayMode(display) : NULL); } /* whether the game is, or is to be, fullscreen, and if so the size in @@ -350,6 +418,10 @@ BOOL platform_screen_mode(long *width, long *height) } #endif +/* the window's scale (display.window_scale, as the window was made or last +resized: platform_display_apply) */ +static long platform_window_scale = -1; + BOOL platform_video_initialize(unsigned long width, unsigned long height) { int scale = (int)config_integer("display.window_scale"); @@ -361,6 +433,7 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) return FALSE; if (scale < 1) scale = 1; + platform_window_scale = scale; #ifdef HALO_ANDROID SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); @@ -402,6 +475,9 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) platform_log("SDL_CreateWindow failed: %s", SDL_GetError()); return FALSE; } +#ifndef HALO_ANDROID + platform_fullscreen_kind_apply(); +#endif platform_gl_context = SDL_GL_CreateContext(platform_window); #ifdef HALO_ANDROID /* ES 3.2 where the driver has it, otherwise the renderer makes do with @@ -430,6 +506,31 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) return TRUE; } +/* display.fullscreen, display.window_scale (when it changes: the window can +be resized) and display.vsync, as Settings has written them */ +void platform_display_apply(void) +{ +#ifndef HALO_ANDROID + BOOL fullscreen = platform_fullscreen_setting(); + long scale = config_integer("display.window_scale"); + + if (!platform_window) + return; + platform_fullscreen_kind_apply(); + if (((SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) != 0) != (fullscreen != FALSE)) + SDL_SetWindowFullscreen(platform_window, fullscreen ? true : false); + if (scale != platform_window_scale && scale >= 1) + { + platform_window_scale = scale; + SDL_SetWindowSize(platform_window, (int)(640 * scale), (int)(480 * scale)); + } +#else + if (!platform_window) + return; +#endif + SDL_GL_SetSwapInterval(config_boolean("display.vsync") ? 1 : 0); +} + void platform_video_drawable_size(int *width, int *height) { SDL_GetWindowSizeInPixels(platform_window, width, height); @@ -442,12 +543,14 @@ can hang (Intel's Raptor Lake graphics, whose reset then takes the desktop with it); the limit gives it a rest every frame. */ static Uint64 frame_interval_ns(void) { - static int vsync = -1; + static int vsync; static long maximum; + static unsigned long read_at = (unsigned long)-1; float rate; - if (vsync < 0) + if (read_at != config_changes()) { + read_at = config_changes(); vsync = config_boolean("display.vsync"); maximum = config_integer("display.max_fps"); } @@ -666,6 +769,23 @@ static BOOL platform_text_has_invite_link(const char *text) return FALSE; } +/* the clipboard's text (the menus' text fields' Ctrl+V), and text put on it +(the server settings' invite link); 0 if there is none. The main thread's */ +int platform_clipboard_get(char *text, int size) +{ + char *clipboard = SDL_GetClipboardText(); + int got = clipboard && *clipboard; + + snprintf(text, (size_t)size, "%s", got ? clipboard : ""); + SDL_free(clipboard); + return got; +} + +void platform_clipboard_set(const char *text) +{ + SDL_SetClipboardText(text); +} + /* puts a new invite on the clipboard, and joins one found there when the game comes to the front */ static void platform_invite_clipboard(BOOL look) @@ -759,6 +879,20 @@ static void platform_show_pending_message(void) /* ---------- events */ +/* quits as closing the window does, when the events are next read (the +menus' Quit: port/linux/game/menu_functions.c); Android's menus have none, +as the system closes its apps */ +void platform_request_quit(void) +{ +#ifndef HALO_ANDROID + SDL_Event event; + + memset(&event, 0, sizeof(event)); + event.type = SDL_EVENT_QUIT; + SDL_PushEvent(&event); +#endif +} + void platform_scoreboard_scroll(int open, long *notches, long *pages) { Uint64 now = SDL_GetTicks(); @@ -823,6 +957,16 @@ void platform_pump_events(void) if (event.key.down) keys_pressed[event.key.scancode] = 1; } + if (binding_capture == _binding_capture_waiting && event.key.down && !event.key.repeat && + event.key.scancode != SDL_SCANCODE_F11 && event.key.scancode != SDL_SCANCODE_F12) + { + binding_capture = _binding_capture_taken; + binding_taken_ms = SDL_GetTicks(); + binding_capture_result = event.key.scancode == SDL_SCANCODE_ESCAPE ? 3 : + event.key.scancode == SDL_SCANCODE_DELETE ? 2 : 1; + binding_captured_input = event.key.scancode; + break; + } queue_keystroke(&event.key); if (SDL_GetTicks() < scoreboard_open_until_ms && event.key.down && (event.key.scancode == SDL_SCANCODE_PAGEUP || event.key.scancode == SDL_SCANCODE_PAGEDOWN)) @@ -861,6 +1005,22 @@ void platform_pump_events(void) break; case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: + if (event.button.button < 32) + { + if (event.button.down) + mouse_buttons_down |= 1u << event.button.button; + else + mouse_buttons_down &= ~(1u << event.button.button); + } + if (binding_capture == _binding_capture_waiting && event.button.down && + event.button.button < PLATFORM_MOUSE_BUTTON_COUNT) + { + binding_capture = _binding_capture_taken; + binding_taken_ms = SDL_GetTicks(); + binding_capture_result = 1; + binding_captured_input = INPUT_MOUSE + event.button.button; + break; + } #ifndef HALO_ANDROID /* clicks in the menus go to the pointer; a button held down when the menu closes stays up until pressed again, so the click @@ -881,9 +1041,21 @@ void platform_pump_events(void) } #endif if (event.button.button < PLATFORM_MOUSE_BUTTON_COUNT) + { input_state.mouse_buttons[event.button.button] = event.button.down; + if (event.button.down) + mouse_buttons_pressed[event.button.button] = 1; + } break; case SDL_EVENT_MOUSE_WHEEL: + if (binding_capture == _binding_capture_waiting && event.wheel.y != 0.0f) + { + binding_capture = _binding_capture_taken; + binding_taken_ms = SDL_GetTicks(); + binding_capture_result = 1; + binding_captured_input = event.wheel.y > 0.0f ? INPUT_WHEEL_UP : INPUT_WHEEL_DOWN; + break; + } if (SDL_GetTicks() < scoreboard_open_until_ms) { /* whole notches, up (away) scrolling up */ @@ -923,6 +1095,7 @@ void platform_pump_events(void) case SDL_EVENT_WINDOW_FOCUS_LOST: memset(input_state.keys, 0, sizeof(input_state.keys)); memset(input_state.mouse_buttons, 0, sizeof(input_state.mouse_buttons)); + memset(mouse_buttons_pressed, 0, sizeof(mouse_buttons_pressed)); input_state.focused = FALSE; break; case SDL_EVENT_WINDOW_FOCUS_GAINED: @@ -945,6 +1118,38 @@ void platform_pump_events(void) platform_invite_clipboard(look_at_clipboard); } +void platform_menus_set_active(BOOL active) +{ + pthread_mutex_lock(&input_lock); + input_state.menus = active; + pthread_mutex_unlock(&input_lock); +} + +void platform_binding_capture_begin(void) +{ + pthread_mutex_lock(&input_lock); + binding_capture = _binding_capture_waiting; + binding_settling = TRUE; + binding_polled_ms = SDL_GetTicks(); + pthread_mutex_unlock(&input_lock); +} + +int platform_binding_capture_poll(int *input) +{ + int result = 0; + + pthread_mutex_lock(&input_lock); + binding_polled_ms = SDL_GetTicks(); + if (binding_capture == _binding_capture_taken) + { + result = binding_capture_result; + *input = binding_captured_input; + binding_capture = _binding_capture_idle; + } + pthread_mutex_unlock(&input_lock); + return result; +} + #ifndef HALO_ANDROID /* ---------- the menus' pointer */ @@ -963,6 +1168,7 @@ void platform_ui_pointer_set_active(BOOL active) input_state.mouse_dy = 0.0f; input_state.mouse_wheel = 0.0f; memset(input_state.mouse_buttons, 0, sizeof(input_state.mouse_buttons)); + memset(mouse_buttons_pressed, 0, sizeof(mouse_buttons_pressed)); pthread_mutex_unlock(&input_lock); platform_mouse_capture(!active && !input_state.mouse_released); if (active) @@ -1004,6 +1210,37 @@ void platform_input_read(struct platform_input_state *state, BOOL consume_motion { pthread_mutex_lock(&input_lock); *state = input_state; + /* (rebinding: nothing reaches the controller until the input is taken + and every key and button is up again) */ + if (binding_capture != _binding_capture_idle || binding_settling) + { + int scancode; + BOOL held = mouse_buttons_down != 0; + + for (scancode = 0; scancode < SDL_SCANCODE_COUNT && !held; scancode++) + held = input_state.keys[scancode] != 0; + if (binding_capture == _binding_capture_taken && SDL_GetTicks() - binding_taken_ms > BINDING_UNCLAIMED_MS) + binding_capture = _binding_capture_idle; + if (binding_capture == _binding_capture_waiting && SDL_GetTicks() - binding_polled_ms > BINDING_ABANDONED_MS) + binding_capture = _binding_capture_idle; + if (binding_capture == _binding_capture_idle && !held) + binding_settling = FALSE; + memset(state->keys, 0, sizeof(state->keys)); + memset(state->mouse_buttons, 0, sizeof(state->mouse_buttons)); + memset(keys_pressed, 0, sizeof(keys_pressed)); + memset(mouse_buttons_pressed, 0, sizeof(mouse_buttons_pressed)); + state->mouse_wheel = 0.0f; + /* (and the motion of the while, which would otherwise pile up for + the aim) */ + state->mouse_dx = state->mouse_dy = 0.0f; + if (consume_motion) + { + input_state.mouse_dx = input_state.mouse_dy = 0.0f; + input_state.mouse_wheel = 0.0f; + } + pthread_mutex_unlock(&input_lock); + return; + } if (consume_motion) { int scancode; @@ -1013,6 +1250,11 @@ void platform_input_read(struct platform_input_state *state, BOOL consume_motion state->keys[scancode] |= keys_pressed[scancode]; keys_pressed[scancode] = 0; } + for (scancode = 0; scancode < PLATFORM_MOUSE_BUTTON_COUNT; scancode++) + { + state->mouse_buttons[scancode] |= mouse_buttons_pressed[scancode]; + mouse_buttons_pressed[scancode] = 0; + } } if (consume_motion) { diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index f2361790..6ecd3b36 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -9,6 +9,7 @@ controller emulation (see sdl_platform.c). #define __HALO_LINUX_SDL_PLATFORM_H #include +#include #define PLATFORM_MOUSE_BUTTON_COUNT 8 @@ -23,8 +24,31 @@ struct platform_input_state /* the mouse drives the menus' pointer (platform_ui_pointer_set_active) instead of the controller */ BOOL ui_pointer; + /* a menu is up (platform_menus_set_active): the keys drive the first + controller, to move about it, instead of the player's actions */ + BOOL menus; }; +/* an input of the keyboard and mouse's controls (xinput_sdl.c): a scancode, +a mouse button (INPUT_MOUSE + SDL_BUTTON_*), or the wheel */ +#define INPUT_MOUSE SDL_SCANCODE_COUNT +#define INPUT_WHEEL (INPUT_MOUSE + PLATFORM_MOUSE_BUTTON_COUNT) +#define INPUT_WHEEL_UP (INPUT_WHEEL + 1) +#define INPUT_WHEEL_DOWN (INPUT_WHEEL + 2) +/* an input by its name in config.toml ("W", "Mouse Left"), -1 if none */ +int halo_input_from_name(const char *name); +void halo_input_name(int input, char *name, size_t size); + +/* whether a menu is up (halo_ui_pointer_update, every frame) */ +void platform_menus_set_active(BOOL active); +/* rebinding a control (Settings > Controls Setup): from now the next key, +mouse button or wheel turn is taken, and none reaches the game or the +menus; nor does the keyboard after, until every key and button is up */ +void platform_binding_capture_begin(void); +/* 0 while waiting; else the capture ends: 1 with the input, 2 for none +(Delete), 3 if cancelled (Escape) */ +int platform_binding_capture_poll(int *input); + struct platform_keystroke { BYTE virtual_key; @@ -39,6 +63,9 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height); BOOL platform_screen_mode(long *width, long *height); #endif void platform_video_drawable_size(int *width, int *height); +/* the window's mode and size and V-Sync, from config.toml as Settings has +just written it (the main thread's) */ +void platform_display_apply(void); void platform_video_swap(void); /* frames between the 30 Hz ticks at the display's refresh rate, unless display.interpolation is false (port/linux/game/render_interpolation.c) */ diff --git a/port/linux/src/text_hires.c b/port/linux/src/text_hires.c index 82a0bb90..4796f3b7 100644 --- a/port/linux/src/text_hires.c +++ b/port/linux/src/text_hires.c @@ -75,10 +75,14 @@ static int names_differ(const char *a, const char *b) static int text_enabled(void) { - static int enabled = -1; + static int enabled; + static unsigned long read_at = (unsigned long)-1; - if (enabled < 0) + if (read_at != config_changes()) + { + read_at = config_changes(); enabled = config_boolean("display.high_res_text"); + } return enabled; } diff --git a/port/linux/src/xbox_textures.c b/port/linux/src/xbox_textures.c index 97fbf526..eb587bfc 100644 --- a/port/linux/src/xbox_textures.c +++ b/port/linux/src/xbox_textures.c @@ -16,6 +16,7 @@ memory_watch.c detects that by write-protecting the pages. #include "xgpu.h" #include "hud_hires.h" +#include "menu_files.h" #include "text_hires.h" #include "port_config.h" @@ -714,6 +715,18 @@ static GLuint texture_entry_result(struct texture_entry *entry, GLenum *target, return atlas; } } + /* (a menu's bitmap: menu_files.h) */ + { + unsigned long levels; + GLuint art = menu_art_texture(entry->data, &levels); + + if (art) + { + description->levels = levels; + description->hires = TRUE; + return art; + } + } if (entry->override >= 0) { GLuint texture = hud_hires_override_texture(entry->override, &description->levels); diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index 68e32a0a..06672577 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -6,20 +6,24 @@ Xbox controllers and the debug keyboard for the Linux build. Port 0 is always connected: it is the keyboard and mouse, merged with the first SDL gamepad when one is present. Further SDL gamepads take ports 1-3. -Keyboard and mouse (port 0): - W A S D left stick arrows D-pad - mouse aim (see halo_linux_mouse_look) - left button right trigger right button, G left trigger - space, enter A F, backspace, X1 B - E, R X tab, wheel Y - Q white X black - left ctrl, C left stick click Z, middle button right stick click - escape start F1 back - F12 release or recapture the mouse +In the game the keyboard and mouse are a control scheme of their own +(port/linux/include/halo_keyboard.h): config.toml's [controls] bind each of +the player's actions to up to two keys, mouse buttons or wheel turns +(Settings > Controls Setup changes them), and the game takes the actions +held (halo_keyboard_actions) with the controller's. They press none of its +buttons, but for the pause menu's and the scoreboard's (Start and Back). -In the menus the mouse is free and drives a pointer instead -(port/linux/include/halo_ui_pointer.h, source/interface/ui_widget.c): its -motion, buttons and wheel do not reach the controller then. +In the menus the keys drive the controller, to move about them: + arrows D-pad W A S D left stick + space, enter A escape, backspace B + delete, E X tab Y + F1 back +(keys held as the game and the menus switch count only once let go of), the +on-screen keyboard takes what is typed, and the mouse is free and drives a +pointer +(port/linux/include/halo_ui_pointer.h, source/interface/ui_widget.c). +F11 switches between fullscreen and the window, and F12 releases or +recaptures the mouse, always. Mouse aim does not go through the right stick: the game's look code asks halo_linux_mouse_look for the motion since its last call and adds it to the @@ -35,9 +39,11 @@ drive the controller. #include "platform.h" #include "sdl_platform.h" #include "port_config.h" +#include "halo_keyboard.h" #include #include +#include #include #include @@ -78,6 +84,8 @@ Y is held, and whether a scroll is under way */ static Uint64 wheel_moved_ms = 0; static Uint64 wheel_press_until_ms = 0; static BOOL wheel_scrolling = FALSE; +/* the way the scroll under way turns: 1 up (away), -1 down */ +static int wheel_direction = 0; /* when port 0's aim last moved, by the mouse and by the right stick (halo_linux_mouse_aiming) */ static Uint64 mouse_aimed_ms = 0; @@ -87,15 +95,23 @@ static Uint64 stick_aimed_ms = 0; worn stick's drift */ #define STICK_AIMING_DEFLECTION 8000 +/* (input.mouse_vertical_sensitivity, read with it) */ +static float vertical_sensitivity = 1.0f; + static float mouse_sensitivity(void) { - static float sensitivity = -1.0f; + static float sensitivity; + static unsigned long read_at = (unsigned long)-1; - if (sensitivity < 0.0f) + if (read_at != config_changes()) { + read_at = config_changes(); sensitivity = (float)config_real("input.mouse_sensitivity"); if (sensitivity <= 0.0f) sensitivity = 1.0f; + vertical_sensitivity = (float)config_real("input.mouse_vertical_sensitivity"); + if (vertical_sensitivity <= 0.0f) + vertical_sensitivity = sensitivity; } return sensitivity; } @@ -106,15 +122,19 @@ int halo_linux_mouse_look(short gamepad_index, float *yaw, float *pitch) { /* radians per pixel of relative motion at sensitivity 1 */ const float scale = 0.0022f; - static int invert = -1; + static int invert; + static unsigned long read_at = (unsigned long)-1; float x, y; *yaw = 0.0f; *pitch = 0.0f; if (gamepad_index != 0) return FALSE; - if (invert < 0) + if (read_at != config_changes()) + { + read_at = config_changes(); invert = config_boolean("input.invert_mouse"); + } pthread_mutex_lock(&mouse_lock); x = mouse_pending_x; y = mouse_pending_y; @@ -125,7 +145,7 @@ int halo_linux_mouse_look(short gamepad_index, float *yaw, float *pitch) if (x == 0.0f && y == 0.0f) return FALSE; *yaw = -x * scale * mouse_sensitivity(); - *pitch = (invert ? y : -y) * scale * mouse_sensitivity(); + *pitch = (invert ? y : -y) * scale * vertical_sensitivity; return TRUE; } @@ -134,13 +154,17 @@ right stick last did) and input.mouse_aim_assist is off: then the view's magnetism leaves them be (player_control.c); the bullets' autoaim stays */ int halo_linux_mouse_aiming(short gamepad_index) { - static int aim_assist = -1; + static int aim_assist; + static unsigned long read_at = (unsigned long)-1; int aiming; if (gamepad_index != 0) return FALSE; - if (aim_assist < 0) + if (read_at != config_changes()) + { + read_at = config_changes(); aim_assist = config_boolean("input.mouse_aim_assist"); + } if (aim_assist) return FALSE; pthread_mutex_lock(&mouse_lock); @@ -180,6 +204,52 @@ static BYTE analog(BOOL down) return down ? 0xff : 0x00; } +/* the game's on-screen keyboard is up (platform_text_typing): the keys type +into it (XInputDebugGetKeystroke passes them to the game), but for the +arrows, which move about it, enter (Done, once let go of since it came up) +and escape (cancel) */ +static BOOL text_typing; +static BOOL text_typing_enter_armed; +/* (the on-screen keyboard's, and a menu's text field's: menu_functions.c) */ +static BOOL text_typing_keyboard, text_typing_field; + +static void text_typing_update(void) +{ + BOOL typing = text_typing_keyboard || text_typing_field; + + if (typing && !text_typing) + text_typing_enter_armed = FALSE; + text_typing = typing; +} + +void platform_text_typing(int typing) +{ + text_typing_keyboard = typing != 0; + text_typing_update(); +} + +void platform_text_field(int typing) +{ + text_typing_field = typing != 0; + text_typing_update(); +} + +static void typing_gamepad(const struct platform_input_state *input, XINPUT_GAMEPAD *pad) +{ + const unsigned char *k = input->keys; + BOOL enter = k[SDL_SCANCODE_RETURN] || k[SDL_SCANCODE_KP_ENTER]; + + if (k[SDL_SCANCODE_UP]) pad->wButtons |= XINPUT_GAMEPAD_DPAD_UP; + if (k[SDL_SCANCODE_DOWN]) pad->wButtons |= XINPUT_GAMEPAD_DPAD_DOWN; + if (k[SDL_SCANCODE_LEFT]) pad->wButtons |= XINPUT_GAMEPAD_DPAD_LEFT; + if (k[SDL_SCANCODE_RIGHT]) pad->wButtons |= XINPUT_GAMEPAD_DPAD_RIGHT; + if (!enter) + text_typing_enter_armed = TRUE; + else if (text_typing_enter_armed) + pad->wButtons |= XINPUT_GAMEPAD_START; + pad->bAnalogButtons[XINPUT_GAMEPAD_B] |= analog(k[SDL_SCANCODE_ESCAPE]); +} + static void keyboard_gamepad(const struct platform_input_state *input, XINPUT_GAMEPAD *pad) { const unsigned char *k = input->keys; @@ -187,6 +257,12 @@ static void keyboard_gamepad(const struct platform_input_state *input, XINPUT_GA const unsigned char *m = input->mouse_buttons; int x = 0, y = 0; + if (text_typing) + { + typing_gamepad(input, pad); + return; + } + if (k[SDL_SCANCODE_D]) x++; if (k[SDL_SCANCODE_A]) x--; if (k[SDL_SCANCODE_W]) y++; @@ -204,25 +280,209 @@ static void keyboard_gamepad(const struct platform_input_state *input, XINPUT_GA if (k[SDL_SCANCODE_DOWN]) pad->wButtons |= XINPUT_GAMEPAD_DPAD_DOWN; if (k[SDL_SCANCODE_LEFT]) pad->wButtons |= XINPUT_GAMEPAD_DPAD_LEFT; if (k[SDL_SCANCODE_RIGHT]) pad->wButtons |= XINPUT_GAMEPAD_DPAD_RIGHT; - if (k[SDL_SCANCODE_ESCAPE]) pad->wButtons |= XINPUT_GAMEPAD_START; if (k[SDL_SCANCODE_F1]) pad->wButtons |= XINPUT_GAMEPAD_BACK; - if (k[SDL_SCANCODE_LCTRL] || k[SDL_SCANCODE_C]) pad->wButtons |= XINPUT_GAMEPAD_LEFT_THUMB; - if (k[SDL_SCANCODE_Z] || (mouse && m[SDL_BUTTON_MIDDLE])) pad->wButtons |= XINPUT_GAMEPAD_RIGHT_THUMB; + /* (escape backs out, as backspace does: the pause menu's B resumes the + game, the main menu's asks to quit; Start would choose, as A does) */ pad->bAnalogButtons[XINPUT_GAMEPAD_A] |= analog(k[SDL_SCANCODE_SPACE] || k[SDL_SCANCODE_RETURN] || k[SDL_SCANCODE_KP_ENTER]); - pad->bAnalogButtons[XINPUT_GAMEPAD_B] |= analog(k[SDL_SCANCODE_F] || k[SDL_SCANCODE_BACKSPACE] || + pad->bAnalogButtons[XINPUT_GAMEPAD_B] |= analog(k[SDL_SCANCODE_ESCAPE] || k[SDL_SCANCODE_BACKSPACE] || (mouse && m[SDL_BUTTON_X1])); #ifdef HALO_ANDROID /* the system back key (gesture or button) backs out of menus */ pad->bAnalogButtons[XINPUT_GAMEPAD_B] |= analog(k[SDL_SCANCODE_AC_BACK]); #endif - pad->bAnalogButtons[XINPUT_GAMEPAD_X] |= analog(k[SDL_SCANCODE_E] || k[SDL_SCANCODE_R]); - pad->bAnalogButtons[XINPUT_GAMEPAD_Y] |= analog(k[SDL_SCANCODE_TAB] || SDL_GetTicks() < wheel_press_until_ms); - pad->bAnalogButtons[XINPUT_GAMEPAD_WHITE] |= analog(k[SDL_SCANCODE_Q]); - pad->bAnalogButtons[XINPUT_GAMEPAD_BLACK] |= analog(k[SDL_SCANCODE_X]); - pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] |= analog(k[SDL_SCANCODE_G] || (mouse && m[SDL_BUTTON_RIGHT])); - pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] |= analog(mouse && m[SDL_BUTTON_LEFT]); + pad->bAnalogButtons[XINPUT_GAMEPAD_X] |= analog(k[SDL_SCANCODE_DELETE] || k[SDL_SCANCODE_E]); + pad->bAnalogButtons[XINPUT_GAMEPAD_Y] |= analog(k[SDL_SCANCODE_TAB]); +} + +/* the keys held when the game and the menus switch count as up until let go +of: the escape that opens the pause menu does not also back out of it, nor +the one that closes it pause the game again */ +static void keys_held_over_switch(struct platform_input_state *input) +{ + static unsigned char held[SDL_SCANCODE_COUNT]; + static int menus = -1; + int scancode; + + if (menus != (input->menus != FALSE)) + { + menus = input->menus != FALSE; + memcpy(held, input->keys, sizeof(held)); + } + for (scancode = 0; scancode < SDL_SCANCODE_COUNT; scancode++) + { + if (!input->keys[scancode]) + held[scancode] = 0; + else if (held[scancode]) + input->keys[scancode] = 0; + } +} + +/* ---------- the keyboard and mouse's own controls */ + +#define MAXIMUM_BINDINGS 2 + +static const char *const binding_settings[NUMBER_OF_HALO_KEYBOARD_ACTIONS] = +{ + "controls.move_forward", "controls.move_backward", "controls.strafe_left", "controls.strafe_right", + "controls.jump", "controls.crouch", "controls.fire", "controls.throw_grenade", "controls.melee", + "controls.reload", "controls.zoom", "controls.switch_weapon", "controls.switch_grenade", "controls.action", + "controls.flashlight", "controls.scoreboard", "controls.pause", +}; + +static const struct +{ + const char *name; + int input; +} named_inputs[] = +{ + /* (SDL's names are "," and "Keypad ,", which a list of bindings, split + at commas, cannot hold) */ + { "Comma", SDL_SCANCODE_COMMA }, + { "Keypad Comma", SDL_SCANCODE_KP_COMMA }, + { "Mouse Left", INPUT_MOUSE + SDL_BUTTON_LEFT }, + { "Mouse Right", INPUT_MOUSE + SDL_BUTTON_RIGHT }, + { "Mouse Middle", INPUT_MOUSE + SDL_BUTTON_MIDDLE }, + { "Mouse 4", INPUT_MOUSE + SDL_BUTTON_X1 }, + { "Mouse 5", INPUT_MOUSE + SDL_BUTTON_X2 }, + { "Wheel", INPUT_WHEEL }, + { "Wheel Up", INPUT_WHEEL_UP }, + { "Wheel Down", INPUT_WHEEL_DOWN }, +}; + +/* the bindings, read again when config.toml changes; -1 for none */ +static int bindings[NUMBER_OF_HALO_KEYBOARD_ACTIONS][MAXIMUM_BINDINGS]; +static unsigned long bindings_read_at = (unsigned long)-1; +static unsigned long keyboard_actions_held; + +/* (the C library's case: SDL's string functions are not the Android +guest's) */ +static int same_name(const char *a, const char *b) +{ + for (; *a && *b; a++, b++) + { + if ((*a >= 'a' && *a <= 'z' ? *a - 32 : *a) != (*b >= 'a' && *b <= 'z' ? *b - 32 : *b)) + return FALSE; + } + return *a == *b; +} + +int halo_input_from_name(const char *name) +{ + SDL_Scancode scancode; + size_t index; + + for (index = 0; index < sizeof(named_inputs) / sizeof(named_inputs[0]); index++) + { + if (same_name(name, named_inputs[index].name)) + return named_inputs[index].input; + } + /* (the other mouse buttons, as halo_input_name names them) */ + if (!strncmp(name, "Mouse ", 6) && name[6] >= '1' && name[6] <= '9' && !name[7] && + name[6] - '0' < PLATFORM_MOUSE_BUTTON_COUNT) + { + return INPUT_MOUSE + (name[6] - '0'); + } + scancode = SDL_GetScancodeFromName(name); + return scancode != SDL_SCANCODE_UNKNOWN ? (int)scancode : -1; +} + +void halo_input_name(int input, char *name, size_t size) +{ + size_t index; + + for (index = 0; index < sizeof(named_inputs) / sizeof(named_inputs[0]); index++) + { + if (named_inputs[index].input == input) + { + snprintf(name, size, "%s", named_inputs[index].name); + return; + } + } + if (input >= 0 && input < SDL_SCANCODE_COUNT && *SDL_GetScancodeName((SDL_Scancode)input)) + snprintf(name, size, "%s", SDL_GetScancodeName((SDL_Scancode)input)); + else if (input >= INPUT_MOUSE && input < INPUT_WHEEL) + snprintf(name, size, "Mouse %d", input - INPUT_MOUSE); + else + snprintf(name, size, "%s", ""); +} + +static void bindings_read(void) +{ + int action; + + if (bindings_read_at == config_changes()) + return; + bindings_read_at = config_changes(); + for (action = 0; action < NUMBER_OF_HALO_KEYBOARD_ACTIONS; action++) + { + const char *text = config_string(binding_settings[action]); + int slot; + + for (slot = 0; slot < MAXIMUM_BINDINGS; slot++) + { + char name[64]; + size_t length; + + bindings[action][slot] = -1; + while (*text == ' ' || *text == ',') + text++; + length = strcspn(text, ","); + if (!length) + continue; + snprintf(name, sizeof(name), "%.*s", (int)length, text); + while (*name && name[strlen(name) - 1] == ' ') + name[strlen(name) - 1] = 0; + bindings[action][slot] = halo_input_from_name(name); + if (bindings[action][slot] < 0) + platform_log("controls: %s has no key or button named \"%s\"", binding_settings[action], name); + text += length; + } + } +} + +static BOOL input_held(const struct platform_input_state *input, int code) +{ + BOOL wheel = SDL_GetTicks() < wheel_press_until_ms; + + if (code < 0) + return FALSE; + if (code < SDL_SCANCODE_COUNT) + return input->keys[code] != 0; + if (code < INPUT_WHEEL) + return !input->mouse_released && input->mouse_buttons[code - INPUT_MOUSE]; + if (code == INPUT_WHEEL) + return wheel; + return wheel && wheel_direction == (code == INPUT_WHEEL_UP ? 1 : -1); +} + +/* in the game: the actions held, and the controller's Start and Back for +the pause menu and the scoreboard */ +static void keyboard_controls(const struct platform_input_state *input, XINPUT_GAMEPAD *pad) +{ + unsigned long held = 0; + int action, slot; + + bindings_read(); + for (action = 0; action < NUMBER_OF_HALO_KEYBOARD_ACTIONS; action++) + { + for (slot = 0; slot < MAXIMUM_BINDINGS; slot++) + { + if (input_held(input, bindings[action][slot])) + held |= 1UL << action; + } + } + if (held & (1UL << HALO_KEYBOARD_PAUSE)) + pad->wButtons |= XINPUT_GAMEPAD_START; + if (held & (1UL << HALO_KEYBOARD_SCOREBOARD)) + pad->wButtons |= XINPUT_GAMEPAD_BACK; + keyboard_actions_held = held; +} + +unsigned long halo_keyboard_actions(short controller_index) +{ + return controller_index == 0 ? keyboard_actions_held : 0; } /* A scroll of the wheel switches weapons once: it holds Y for WHEEL_PRESS_MS @@ -313,6 +573,7 @@ static void wheel_update(void) if (fabsf(mouse_wheel_accumulated) >= 1.0f) { wheel_scrolling = TRUE; + wheel_direction = mouse_wheel_accumulated > 0.0f ? 1 : -1; wheel_press_until_ms = now + WHEEL_PRESS_MS; } } @@ -543,8 +804,15 @@ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state) platform_input_read(&input, TRUE); mouse_poll(&input); wheel_update(); + keyboard_actions_held = 0; + keys_held_over_switch(&input); if (!console_is_active()) - keyboard_gamepad(&input, &state->Gamepad); + { + if (input.menus) + keyboard_gamepad(&input, &state->Gamepad); + else + keyboard_controls(&input, &state->Gamepad); + } if (count > 0) sdl_gamepad_state(gamepads[0], &state->Gamepad); test_input_gamepad(&state->Gamepad); @@ -607,8 +875,11 @@ DWORD WINAPI XInputDebugGetKeystroke(PXINPUT_DEBUG_KEYSTROKE keystroke) { BOOL key_up = (next.flags & XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP) != 0; - /* key ups always pass, so no key is left latched down */ - if (key_up || next.virtual_key == VK_OEM_3_BACKQUOTE || console_is_active()) + /* key ups always pass, so no key is left latched down; while typing, + escape does not (it cancels, as B: the game's own escape leaves the + menus, main.c) */ + if (key_up || next.virtual_key == VK_OEM_3_BACKQUOTE || console_is_active() || + (text_typing && next.virtual_key != 0x1B /* escape */)) { keystroke->VirtualKey = next.virtual_key; keystroke->Ascii = next.ascii; diff --git a/port/third_party/expat/COPYING b/port/third_party/expat/COPYING new file mode 100644 index 00000000..c6d184a8 --- /dev/null +++ b/port/third_party/expat/COPYING @@ -0,0 +1,21 @@ +Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper +Copyright (c) 2001-2025 Expat maintainers + +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. diff --git a/port/third_party/expat/README.md b/port/third_party/expat/README.md new file mode 100644 index 00000000..c95f2da9 --- /dev/null +++ b/port/third_party/expat/README.md @@ -0,0 +1,15 @@ +# Expat + +A streaming XML parser in C, by James Clark, Sebastian Pipping and its other +authors, MIT licensed (see `COPYING`). + +Upstream: https://github.com/libexpat/libexpat, release `R_2_8_5` +(`expat-2.8.5.tar.xz`, SHA-256 +1e727b8933ec51a77a9a9d9afcf8e688bce45d907c13e36ab7393fe36e703182, signed by +Sebastian Pipping). The `.c` and `.h` files are copied unchanged from its +`lib/` directory (the parser, the tokenizer and Windows' `rand_s` entropy); +`expat_config.h` is ours. + +The native ports read the menus' XML files with it +(`port/linux/src/menu_files.c`); it builds into the Linux, Windows and +Android platform layers. diff --git a/port/third_party/expat/ascii.h b/port/third_party/expat/ascii.h new file mode 100644 index 00000000..1d9cf70b --- /dev/null +++ b/port/third_party/expat/ascii.h @@ -0,0 +1,125 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1999-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2007 Karl Waclawek + Copyright (c) 2017 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#define ASCII_A 0x41 +#define ASCII_B 0x42 +#define ASCII_C 0x43 +#define ASCII_D 0x44 +#define ASCII_E 0x45 +#define ASCII_F 0x46 +#define ASCII_G 0x47 +#define ASCII_H 0x48 +#define ASCII_I 0x49 +#define ASCII_J 0x4A +#define ASCII_K 0x4B +#define ASCII_L 0x4C +#define ASCII_M 0x4D +#define ASCII_N 0x4E +#define ASCII_O 0x4F +#define ASCII_P 0x50 +#define ASCII_Q 0x51 +#define ASCII_R 0x52 +#define ASCII_S 0x53 +#define ASCII_T 0x54 +#define ASCII_U 0x55 +#define ASCII_V 0x56 +#define ASCII_W 0x57 +#define ASCII_X 0x58 +#define ASCII_Y 0x59 +#define ASCII_Z 0x5A + +#define ASCII_a 0x61 +#define ASCII_b 0x62 +#define ASCII_c 0x63 +#define ASCII_d 0x64 +#define ASCII_e 0x65 +#define ASCII_f 0x66 +#define ASCII_g 0x67 +#define ASCII_h 0x68 +#define ASCII_i 0x69 +#define ASCII_j 0x6A +#define ASCII_k 0x6B +#define ASCII_l 0x6C +#define ASCII_m 0x6D +#define ASCII_n 0x6E +#define ASCII_o 0x6F +#define ASCII_p 0x70 +#define ASCII_q 0x71 +#define ASCII_r 0x72 +#define ASCII_s 0x73 +#define ASCII_t 0x74 +#define ASCII_u 0x75 +#define ASCII_v 0x76 +#define ASCII_w 0x77 +#define ASCII_x 0x78 +#define ASCII_y 0x79 +#define ASCII_z 0x7A + +#define ASCII_0 0x30 +#define ASCII_1 0x31 +#define ASCII_2 0x32 +#define ASCII_3 0x33 +#define ASCII_4 0x34 +#define ASCII_5 0x35 +#define ASCII_6 0x36 +#define ASCII_7 0x37 +#define ASCII_8 0x38 +#define ASCII_9 0x39 + +#define ASCII_TAB 0x09 +#define ASCII_SPACE 0x20 +#define ASCII_EXCL 0x21 +#define ASCII_QUOT 0x22 +#define ASCII_AMP 0x26 +#define ASCII_APOS 0x27 +#define ASCII_MINUS 0x2D +#define ASCII_PERIOD 0x2E +#define ASCII_COLON 0x3A +#define ASCII_SEMI 0x3B +#define ASCII_LT 0x3C +#define ASCII_EQUALS 0x3D +#define ASCII_GT 0x3E +#define ASCII_LSQB 0x5B +#define ASCII_RSQB 0x5D +#define ASCII_UNDERSCORE 0x5F +#define ASCII_LPAREN 0x28 +#define ASCII_RPAREN 0x29 +#define ASCII_FF 0x0C +#define ASCII_SLASH 0x2F +#define ASCII_HASH 0x23 +#define ASCII_PIPE 0x7C +#define ASCII_COMMA 0x2C diff --git a/port/third_party/expat/asciitab.h b/port/third_party/expat/asciitab.h new file mode 100644 index 00000000..43af0876 --- /dev/null +++ b/port/third_party/expat/asciitab.h @@ -0,0 +1,68 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2017 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +/* 0x00 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x04 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x08 */ BT_NONXML, BT_S, BT_LF, BT_NONXML, + /* 0x0C */ BT_NONXML, BT_CR, BT_NONXML, BT_NONXML, + /* 0x10 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x14 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x18 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x1C */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x20 */ BT_S, BT_EXCL, BT_QUOT, BT_NUM, + /* 0x24 */ BT_OTHER, BT_PERCNT, BT_AMP, BT_APOS, + /* 0x28 */ BT_LPAR, BT_RPAR, BT_AST, BT_PLUS, + /* 0x2C */ BT_COMMA, BT_MINUS, BT_NAME, BT_SOL, + /* 0x30 */ BT_DIGIT, BT_DIGIT, BT_DIGIT, BT_DIGIT, + /* 0x34 */ BT_DIGIT, BT_DIGIT, BT_DIGIT, BT_DIGIT, + /* 0x38 */ BT_DIGIT, BT_DIGIT, BT_COLON, BT_SEMI, + /* 0x3C */ BT_LT, BT_EQUALS, BT_GT, BT_QUEST, + /* 0x40 */ BT_OTHER, BT_HEX, BT_HEX, BT_HEX, + /* 0x44 */ BT_HEX, BT_HEX, BT_HEX, BT_NMSTRT, + /* 0x48 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x4C */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x50 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x54 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x58 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_LSQB, + /* 0x5C */ BT_OTHER, BT_RSQB, BT_OTHER, BT_NMSTRT, + /* 0x60 */ BT_OTHER, BT_HEX, BT_HEX, BT_HEX, + /* 0x64 */ BT_HEX, BT_HEX, BT_HEX, BT_NMSTRT, + /* 0x68 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x6C */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x70 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x74 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x78 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_OTHER, + /* 0x7C */ BT_VERBAR, BT_OTHER, BT_OTHER, BT_OTHER, diff --git a/port/third_party/expat/expat.h b/port/third_party/expat/expat.h new file mode 100644 index 00000000..4c3851d5 --- /dev/null +++ b/port/third_party/expat/expat.h @@ -0,0 +1,1111 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2000-2005 Fred L. Drake, Jr. + Copyright (c) 2001-2002 Greg Stein + Copyright (c) 2002-2016 Karl Waclawek + Copyright (c) 2016-2026 Sebastian Pipping + Copyright (c) 2016 Cristian Rodríguez + Copyright (c) 2016 Thomas Beutlich + Copyright (c) 2017 Rhodri James + Copyright (c) 2022 Thijs Schreijer + Copyright (c) 2023 Hanno Böck + Copyright (c) 2023 Sony Corporation / Snild Dolkow + Copyright (c) 2024 Taichi Haradaguchi <20001722@ymail.ne.jp> + Copyright (c) 2025 Matthew Fernandez + Copyright (c) 2026 Braian Plaku + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#ifndef Expat_INCLUDED +# define Expat_INCLUDED 1 + +# include // for uint8_t +# include +# include "expat_external.h" + +# ifdef __cplusplus +extern "C" { +# endif + +struct XML_ParserStruct; +typedef struct XML_ParserStruct *XML_Parser; + +typedef unsigned char XML_Bool; +# define XML_TRUE ((XML_Bool)1) +# define XML_FALSE ((XML_Bool)0) + +/* The XML_Status enum gives the possible return values for several + API functions. The preprocessor #defines are included so this + stanza can be added to code that still needs to support older + versions of Expat 1.95.x: + + #ifndef XML_STATUS_OK + #define XML_STATUS_OK 1 + #define XML_STATUS_ERROR 0 + #endif + + Otherwise, the #define hackery is quite ugly and would have been + dropped. +*/ +enum XML_Status { + XML_STATUS_ERROR = 0, +# define XML_STATUS_ERROR XML_STATUS_ERROR + XML_STATUS_OK = 1, +# define XML_STATUS_OK XML_STATUS_OK + XML_STATUS_SUSPENDED = 2 +# define XML_STATUS_SUSPENDED XML_STATUS_SUSPENDED +}; + +enum XML_Error { + XML_ERROR_NONE, + XML_ERROR_NO_MEMORY, + XML_ERROR_SYNTAX, + XML_ERROR_NO_ELEMENTS, + XML_ERROR_INVALID_TOKEN, + XML_ERROR_UNCLOSED_TOKEN, + XML_ERROR_PARTIAL_CHAR, + XML_ERROR_TAG_MISMATCH, + XML_ERROR_DUPLICATE_ATTRIBUTE, + XML_ERROR_JUNK_AFTER_DOC_ELEMENT, + XML_ERROR_PARAM_ENTITY_REF, + XML_ERROR_UNDEFINED_ENTITY, + XML_ERROR_RECURSIVE_ENTITY_REF, + XML_ERROR_ASYNC_ENTITY, + XML_ERROR_BAD_CHAR_REF, + XML_ERROR_BINARY_ENTITY_REF, + XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF, + XML_ERROR_MISPLACED_XML_PI, + XML_ERROR_UNKNOWN_ENCODING, + XML_ERROR_INCORRECT_ENCODING, + XML_ERROR_UNCLOSED_CDATA_SECTION, + XML_ERROR_EXTERNAL_ENTITY_HANDLING, + XML_ERROR_NOT_STANDALONE, + XML_ERROR_UNEXPECTED_STATE, + XML_ERROR_ENTITY_DECLARED_IN_PE, + XML_ERROR_FEATURE_REQUIRES_XML_DTD, + XML_ERROR_CANT_CHANGE_FEATURE_ONCE_PARSING, + /* Added in 1.95.7. */ + XML_ERROR_UNBOUND_PREFIX, + /* Added in 1.95.8. */ + XML_ERROR_UNDECLARING_PREFIX, + XML_ERROR_INCOMPLETE_PE, + XML_ERROR_XML_DECL, + XML_ERROR_TEXT_DECL, + XML_ERROR_PUBLICID, + XML_ERROR_SUSPENDED, + XML_ERROR_NOT_SUSPENDED, + XML_ERROR_ABORTED, + XML_ERROR_FINISHED, + XML_ERROR_SUSPEND_PE, + /* Added in 2.0. */ + XML_ERROR_RESERVED_PREFIX_XML, + XML_ERROR_RESERVED_PREFIX_XMLNS, + XML_ERROR_RESERVED_NAMESPACE_URI, + /* Added in 2.2.1. */ + XML_ERROR_INVALID_ARGUMENT, + /* Added in 2.3.0. */ + XML_ERROR_NO_BUFFER, + /* Added in 2.4.0. */ + XML_ERROR_AMPLIFICATION_LIMIT_BREACH, + /* Added in 2.6.4. */ + XML_ERROR_NOT_STARTED, +}; + +enum XML_Content_Type { + XML_CTYPE_EMPTY = 1, + XML_CTYPE_ANY, + XML_CTYPE_MIXED, + XML_CTYPE_NAME, + XML_CTYPE_CHOICE, + XML_CTYPE_SEQ +}; + +enum XML_Content_Quant { + XML_CQUANT_NONE, + XML_CQUANT_OPT, + XML_CQUANT_REP, + XML_CQUANT_PLUS +}; + +/* If type == XML_CTYPE_EMPTY or XML_CTYPE_ANY, then quant will be + XML_CQUANT_NONE, and the other fields will be zero or NULL. + If type == XML_CTYPE_MIXED, then quant will be NONE or REP and + numchildren will contain number of elements that may be mixed in + and children point to an array of XML_Content cells that will be + all of XML_CTYPE_NAME type with no quantification. + + If type == XML_CTYPE_NAME, then the name points to the name, and + the numchildren field will be zero and children will be NULL. The + quant fields indicates any quantifiers placed on the name. + + CHOICE and SEQ will have name NULL, the number of children in + numchildren and children will point, recursively, to an array + of XML_Content cells. + + The EMPTY, ANY, and MIXED types will only occur at top level. +*/ + +typedef struct XML_cp XML_Content; + +struct XML_cp { + enum XML_Content_Type type; + enum XML_Content_Quant quant; + XML_Char *name; + unsigned int numchildren; + XML_Content *children; +}; + +/* This is called for an element declaration. See above for + description of the model argument. It's the user code's responsibility + to free model when finished with it. See XML_FreeContentModel. + There is no need to free the model from the handler, it can be kept + around and freed at a later stage. +*/ +typedef void(XMLCALL *XML_ElementDeclHandler)(void *userData, + const XML_Char *name, + XML_Content *model); + +XMLPARSEAPI(void) +XML_SetElementDeclHandler(XML_Parser parser, XML_ElementDeclHandler eldecl); + +/* The Attlist declaration handler is called for *each* attribute. So + a single Attlist declaration with multiple attributes declared will + generate multiple calls to this handler. The "default" parameter + may be NULL in the case of the "#IMPLIED" or "#REQUIRED" + keyword. The "isrequired" parameter will be true and the default + value will be NULL in the case of "#REQUIRED". If "isrequired" is + true and default is non-NULL, then this is a "#FIXED" default. +*/ +typedef void(XMLCALL *XML_AttlistDeclHandler)( + void *userData, const XML_Char *elname, const XML_Char *attname, + const XML_Char *att_type, const XML_Char *dflt, int isrequired); + +XMLPARSEAPI(void) +XML_SetAttlistDeclHandler(XML_Parser parser, XML_AttlistDeclHandler attdecl); + +/* The XML declaration handler is called for *both* XML declarations + and text declarations. The way to distinguish is that the version + parameter will be NULL for text declarations. The encoding + parameter may be NULL for XML declarations. The standalone + parameter will be -1, 0, or 1 indicating respectively that there + was no standalone parameter in the declaration, that it was given + as no, or that it was given as yes. +*/ +typedef void(XMLCALL *XML_XmlDeclHandler)(void *userData, + const XML_Char *version, + const XML_Char *encoding, + int standalone); + +XMLPARSEAPI(void) +XML_SetXmlDeclHandler(XML_Parser parser, XML_XmlDeclHandler xmldecl); + +typedef struct { + void *(*malloc_fcn)(size_t size); + void *(*realloc_fcn)(void *ptr, size_t size); + void (*free_fcn)(void *ptr); +} XML_Memory_Handling_Suite; + +/* Constructs a new parser; encoding is the encoding specified by the + external protocol or NULL if there is none specified. +*/ +XMLPARSEAPI(XML_Parser) +XML_ParserCreate(const XML_Char *encoding); + +/* Constructs a new parser and namespace processor. Element type + names and attribute names that belong to a namespace will be + expanded; unprefixed attribute names are never expanded; unprefixed + element type names are expanded only if there is a default + namespace. The expanded name is the concatenation of the namespace + URI, the namespace separator character, and the local part of the + name. If the namespace separator is '\0' then the namespace URI + and the local part will be concatenated without any separator. + It is a programming error to use the separator '\0' with namespace + triplets (see XML_SetReturnNSTriplet). + If a namespace separator is chosen that can be part of a URI or + part of an XML name, splitting an expanded name back into its + 1, 2 or 3 original parts on application level in the element handler + may end up vulnerable, so these are advised against; sane choices for + a namespace separator are e.g. '\n' (line feed) and '|' (pipe). + + Note that Expat does not validate namespace URIs (beyond encoding) + against RFC 3986 today (and is not required to do so with regard to + the XML 1.0 namespaces specification) but it may start doing that + in future releases. Before that, an application using Expat must + be ready to receive namespace URIs containing non-URI characters. +*/ +XMLPARSEAPI(XML_Parser) +XML_ParserCreateNS(const XML_Char *encoding, XML_Char namespaceSeparator); + +/* Constructs a new parser using the memory management suite referred to + by memsuite. If memsuite is NULL, then use the standard library memory + suite. If namespaceSeparator is non-NULL it creates a parser with + namespace processing as described above. The character pointed at + will serve as the namespace separator. + + All further memory operations used for the created parser will come from + the given suite. +*/ +XMLPARSEAPI(XML_Parser) +XML_ParserCreate_MM(const XML_Char *encoding, + const XML_Memory_Handling_Suite *memsuite, + const XML_Char *namespaceSeparator); + +/* Prepare a parser object to be reused. This is particularly + valuable when memory allocation overhead is disproportionately high, + such as when a large number of small documents need to be parsed. + All handlers are cleared from the parser, except for the + unknownEncodingHandler. The parser's external state is re-initialized + except for the values of ns and ns_triplets. + + Added in Expat 1.95.3. +*/ +XMLPARSEAPI(XML_Bool) +XML_ParserReset(XML_Parser parser, const XML_Char *encoding); + +/* atts is array of name/value pairs, terminated by 0; + names and values are 0 terminated. +*/ +typedef void(XMLCALL *XML_StartElementHandler)(void *userData, + const XML_Char *name, + const XML_Char **atts); + +typedef void(XMLCALL *XML_EndElementHandler)(void *userData, + const XML_Char *name); + +/* s is not 0 terminated. */ +typedef void(XMLCALL *XML_CharacterDataHandler)(void *userData, + const XML_Char *s, int len); + +/* target and data are 0 terminated */ +typedef void(XMLCALL *XML_ProcessingInstructionHandler)(void *userData, + const XML_Char *target, + const XML_Char *data); + +/* data is 0 terminated */ +typedef void(XMLCALL *XML_CommentHandler)(void *userData, const XML_Char *data); + +typedef void(XMLCALL *XML_StartCdataSectionHandler)(void *userData); +typedef void(XMLCALL *XML_EndCdataSectionHandler)(void *userData); + +/* This is called for any characters in the XML document for which + there is no applicable handler. This includes both characters that + are part of markup which is of a kind that is not reported + (comments, markup declarations), or characters that are part of a + construct which could be reported but for which no handler has been + supplied. The characters are passed exactly as they were in the XML + document except that they will be encoded in UTF-8 or UTF-16. + Line boundaries are not normalized. Note that a byte order mark + character is not passed to the default handler. There are no + guarantees about how characters are divided between calls to the + default handler: for example, a comment might be split between + multiple calls. +*/ +typedef void(XMLCALL *XML_DefaultHandler)(void *userData, const XML_Char *s, + int len); + +/* This is called for the start of the DOCTYPE declaration, before + any DTD or internal subset is parsed. +*/ +typedef void(XMLCALL *XML_StartDoctypeDeclHandler)(void *userData, + const XML_Char *doctypeName, + const XML_Char *sysid, + const XML_Char *pubid, + int has_internal_subset); + +/* This is called for the end of the DOCTYPE declaration when the + closing > is encountered, but after processing any external + subset. +*/ +typedef void(XMLCALL *XML_EndDoctypeDeclHandler)(void *userData); + +/* This is called for entity declarations. The is_parameter_entity + argument will be non-zero if the entity is a parameter entity, zero + otherwise. + + For internal entities (), value will + be non-NULL and systemId, publicID, and notationName will be NULL. + The value string is NOT null-terminated; the length is provided in + the value_length argument. Since it is legal to have zero-length + values, do not use this argument to test for internal entities. + + For external entities, value will be NULL and systemId will be + non-NULL. The publicId argument will be NULL unless a public + identifier was provided. The notationName argument will have a + non-NULL value only for unparsed entity declarations. + + Note that is_parameter_entity can't be changed to XML_Bool, since + that would break binary compatibility. +*/ +typedef void(XMLCALL *XML_EntityDeclHandler)( + void *userData, const XML_Char *entityName, int is_parameter_entity, + const XML_Char *value, int value_length, const XML_Char *base, + const XML_Char *systemId, const XML_Char *publicId, + const XML_Char *notationName); + +XMLPARSEAPI(void) +XML_SetEntityDeclHandler(XML_Parser parser, XML_EntityDeclHandler handler); + +/* OBSOLETE -- OBSOLETE -- OBSOLETE + This handler has been superseded by the EntityDeclHandler above. + It is provided here for backward compatibility. + + This is called for a declaration of an unparsed (NDATA) entity. + The base argument is whatever was set by XML_SetBase. The + entityName, systemId and notationName arguments will never be + NULL. The other arguments may be. +*/ +typedef void(XMLCALL *XML_UnparsedEntityDeclHandler)( + void *userData, const XML_Char *entityName, const XML_Char *base, + const XML_Char *systemId, const XML_Char *publicId, + const XML_Char *notationName); + +/* This is called for a declaration of notation. The base argument is + whatever was set by XML_SetBase. The notationName will never be + NULL. The other arguments can be. +*/ +typedef void(XMLCALL *XML_NotationDeclHandler)(void *userData, + const XML_Char *notationName, + const XML_Char *base, + const XML_Char *systemId, + const XML_Char *publicId); + +/* When namespace processing is enabled, these are called once for + each namespace declaration. The call to the start and end element + handlers occur between the calls to the start and end namespace + declaration handlers. For an xmlns attribute, prefix will be + NULL. For an xmlns="" attribute, uri will be NULL. +*/ +typedef void(XMLCALL *XML_StartNamespaceDeclHandler)(void *userData, + const XML_Char *prefix, + const XML_Char *uri); + +typedef void(XMLCALL *XML_EndNamespaceDeclHandler)(void *userData, + const XML_Char *prefix); + +/* This is called if the document is not standalone, that is, it has an + external subset or a reference to a parameter entity, but does not + have standalone="yes". If this handler returns XML_STATUS_ERROR, + then processing will not continue, and the parser will return a + XML_ERROR_NOT_STANDALONE error. + If parameter entity parsing is enabled, then in addition to the + conditions above this handler will only be called if the referenced + entity was actually read. +*/ +typedef int(XMLCALL *XML_NotStandaloneHandler)(void *userData); + +/* This is called for a reference to an external parsed general + entity. The referenced entity is not automatically parsed. The + application can parse it immediately or later using + XML_ExternalEntityParserCreate. + + The parser argument is the parser parsing the entity containing the + reference; it can be passed as the parser argument to + XML_ExternalEntityParserCreate. The systemId argument is the + system identifier as specified in the entity declaration; it will + not be NULL. + + The base argument is the system identifier that should be used as + the base for resolving systemId if systemId was relative; this is + set by XML_SetBase; it may be NULL. + + The publicId argument is the public identifier as specified in the + entity declaration, or NULL if none was specified; the whitespace + in the public identifier will have been normalized as required by + the XML spec. + + The context argument specifies the parsing context in the format + expected by the context argument to XML_ExternalEntityParserCreate; + context is valid only until the handler returns, so if the + referenced entity is to be parsed later, it must be copied. + context is NULL only when the entity is a parameter entity. + + The handler should return XML_STATUS_ERROR if processing should not + continue because of a fatal error in the handling of the external + entity. In this case the calling parser will return an + XML_ERROR_EXTERNAL_ENTITY_HANDLING error. + + Note that unlike other handlers the first argument is the parser, + not userData. +*/ +typedef int(XMLCALL *XML_ExternalEntityRefHandler)(XML_Parser parser, + const XML_Char *context, + const XML_Char *base, + const XML_Char *systemId, + const XML_Char *publicId); + +/* This is called in two situations: + 1) An entity reference is encountered for which no declaration + has been read *and* this is not an error. + 2) An internal entity reference is read, but not expanded, because + XML_SetDefaultHandler has been called. + Note: skipped parameter entities in declarations and skipped general + entities in attribute values cannot be reported, because + the event would be out of sync with the reporting of the + declarations or attribute values +*/ +typedef void(XMLCALL *XML_SkippedEntityHandler)(void *userData, + const XML_Char *entityName, + int is_parameter_entity); + +/* This structure is filled in by the XML_UnknownEncodingHandler to + provide information to the parser about encodings that are unknown + to the parser. + + The map[b] member gives information about byte sequences whose + first byte is b. + + If map[b] is c where c is >= 0, then b by itself encodes the + Unicode scalar value c. + + If map[b] is -1, then the byte sequence is malformed. + + If map[b] is -n, where n >= 2, then b is the first byte of an + n-byte sequence that encodes a single Unicode scalar value. + + The data member will be passed as the first argument to the convert + function. + + The convert function is used to convert multibyte sequences; s will + point to a n-byte sequence where map[(unsigned char)*s] == -n. The + convert function must return the Unicode scalar value represented + by this byte sequence or -1 if the byte sequence is malformed. + + The convert function may be NULL if the encoding is a single-byte + encoding, that is if map[b] >= -1 for all bytes b. + + When the parser is finished with the encoding, then if release is + not NULL, it will call release passing it the data member; once + release has been called, the convert function will not be called + again. + + Expat places certain restrictions on the encodings that are supported + using this mechanism. + + 1. Every ASCII character that can appear in a well-formed XML document, + other than the characters + + $@\^`{}~ + + must be represented by a single byte, and that byte must be the + same byte that represents that character in ASCII. + + 2. No character may require more than 4 bytes to encode. + + 3. All characters encoded must have Unicode scalar values <= + 0xFFFF, (i.e., characters that would be encoded by surrogates in + UTF-16 are not allowed). Note that this restriction doesn't + apply to the built-in support for UTF-8 and UTF-16. + + 4. No Unicode character may be encoded by more than one distinct + sequence of bytes. +*/ +typedef struct { + int map[256]; + void *data; + int(XMLCALL *convert)(void *data, const char *s); + void(XMLCALL *release)(void *data); +} XML_Encoding; + +/* This is called for an encoding that is unknown to the parser. + + The encodingHandlerData argument is that which was passed as the + second argument to XML_SetUnknownEncodingHandler. + + The name argument gives the name of the encoding as specified in + the encoding declaration. + + If the callback can provide information about the encoding, it must + fill in the XML_Encoding structure, and return XML_STATUS_OK. + Otherwise it must return XML_STATUS_ERROR. + + If info does not describe a suitable encoding, then the parser will + return an XML_ERROR_UNKNOWN_ENCODING error. +*/ +typedef int(XMLCALL *XML_UnknownEncodingHandler)(void *encodingHandlerData, + const XML_Char *name, + XML_Encoding *info); + +XMLPARSEAPI(void) +XML_SetElementHandler(XML_Parser parser, XML_StartElementHandler start, + XML_EndElementHandler end); + +XMLPARSEAPI(void) +XML_SetStartElementHandler(XML_Parser parser, XML_StartElementHandler handler); + +XMLPARSEAPI(void) +XML_SetEndElementHandler(XML_Parser parser, XML_EndElementHandler handler); + +XMLPARSEAPI(void) +XML_SetCharacterDataHandler(XML_Parser parser, + XML_CharacterDataHandler handler); + +XMLPARSEAPI(void) +XML_SetProcessingInstructionHandler(XML_Parser parser, + XML_ProcessingInstructionHandler handler); +XMLPARSEAPI(void) +XML_SetCommentHandler(XML_Parser parser, XML_CommentHandler handler); + +XMLPARSEAPI(void) +XML_SetCdataSectionHandler(XML_Parser parser, + XML_StartCdataSectionHandler start, + XML_EndCdataSectionHandler end); + +XMLPARSEAPI(void) +XML_SetStartCdataSectionHandler(XML_Parser parser, + XML_StartCdataSectionHandler start); + +XMLPARSEAPI(void) +XML_SetEndCdataSectionHandler(XML_Parser parser, + XML_EndCdataSectionHandler end); + +/* This sets the default handler and also inhibits expansion of + internal entities. These entity references will be passed to the + default handler, or to the skipped entity handler, if one is set. +*/ +XMLPARSEAPI(void) +XML_SetDefaultHandler(XML_Parser parser, XML_DefaultHandler handler); + +/* This sets the default handler but does not inhibit expansion of + internal entities. The entity reference will not be passed to the + default handler. +*/ +XMLPARSEAPI(void) +XML_SetDefaultHandlerExpand(XML_Parser parser, XML_DefaultHandler handler); + +XMLPARSEAPI(void) +XML_SetDoctypeDeclHandler(XML_Parser parser, XML_StartDoctypeDeclHandler start, + XML_EndDoctypeDeclHandler end); + +XMLPARSEAPI(void) +XML_SetStartDoctypeDeclHandler(XML_Parser parser, + XML_StartDoctypeDeclHandler start); + +XMLPARSEAPI(void) +XML_SetEndDoctypeDeclHandler(XML_Parser parser, XML_EndDoctypeDeclHandler end); + +XMLPARSEAPI(void) +XML_SetUnparsedEntityDeclHandler(XML_Parser parser, + XML_UnparsedEntityDeclHandler handler); + +XMLPARSEAPI(void) +XML_SetNotationDeclHandler(XML_Parser parser, XML_NotationDeclHandler handler); + +XMLPARSEAPI(void) +XML_SetNamespaceDeclHandler(XML_Parser parser, + XML_StartNamespaceDeclHandler start, + XML_EndNamespaceDeclHandler end); + +XMLPARSEAPI(void) +XML_SetStartNamespaceDeclHandler(XML_Parser parser, + XML_StartNamespaceDeclHandler start); + +XMLPARSEAPI(void) +XML_SetEndNamespaceDeclHandler(XML_Parser parser, + XML_EndNamespaceDeclHandler end); + +XMLPARSEAPI(void) +XML_SetNotStandaloneHandler(XML_Parser parser, + XML_NotStandaloneHandler handler); + +XMLPARSEAPI(void) +XML_SetExternalEntityRefHandler(XML_Parser parser, + XML_ExternalEntityRefHandler handler); + +/* If a non-NULL value for arg is specified here, then it will be + passed as the first argument to the external entity ref handler + instead of the parser object. +*/ +XMLPARSEAPI(void) +XML_SetExternalEntityRefHandlerArg(XML_Parser parser, void *arg); + +XMLPARSEAPI(void) +XML_SetSkippedEntityHandler(XML_Parser parser, + XML_SkippedEntityHandler handler); + +XMLPARSEAPI(void) +XML_SetUnknownEncodingHandler(XML_Parser parser, + XML_UnknownEncodingHandler handler, + void *encodingHandlerData); + +/* This can be called within a handler for a start element, end + element, processing instruction or character data. It causes the + corresponding markup to be passed to the default handler. +*/ +XMLPARSEAPI(void) +XML_DefaultCurrent(XML_Parser parser); + +/* If do_nst is non-zero, and namespace processing is in effect, and + a name has a prefix (i.e. an explicit namespace qualifier) then + that name is returned as a triplet in a single string separated by + the separator character specified when the parser was created: URI + + sep + local_name + sep + prefix. + + If do_nst is zero, then namespace information is returned in the + default manner (URI + sep + local_name) whether or not the name + has a prefix. + + Note: Calling XML_SetReturnNSTriplet after XML_Parse or + XML_ParseBuffer has no effect. +*/ + +XMLPARSEAPI(void) +XML_SetReturnNSTriplet(XML_Parser parser, int do_nst); + +/* This value is passed as the userData argument to callbacks. */ +XMLPARSEAPI(void) +XML_SetUserData(XML_Parser parser, void *userData); + +/* Returns the last value set by XML_SetUserData or NULL. */ +# define XML_GetUserData(parser) (*(void **)(parser)) + +/* This is equivalent to supplying an encoding argument to + XML_ParserCreate. On success XML_SetEncoding returns non-zero, + zero otherwise. + Note: Calling XML_SetEncoding after XML_Parse or XML_ParseBuffer + has no effect and returns XML_STATUS_ERROR. +*/ +XMLPARSEAPI(enum XML_Status) +XML_SetEncoding(XML_Parser parser, const XML_Char *encoding); + +/* If this function is called, then the parser will be passed as the + first argument to callbacks instead of userData. The userData will + still be accessible using XML_GetUserData. +*/ +XMLPARSEAPI(void) +XML_UseParserAsHandlerArg(XML_Parser parser); + +/* If useDTD == XML_TRUE is passed to this function, then the parser + will assume that there is an external subset, even if none is + specified in the document. In such a case the parser will call the + externalEntityRefHandler with a value of NULL for the systemId + argument (the publicId and context arguments will be NULL as well). + Note: For the purpose of checking WFC: Entity Declared, passing + useDTD == XML_TRUE will make the parser behave as if the document + had a DTD with an external subset. + Note: If this function is called, then this must be done before + the first call to XML_Parse or XML_ParseBuffer, since it will + have no effect after that. Returns + XML_ERROR_CANT_CHANGE_FEATURE_ONCE_PARSING. + Note: If the document does not have a DOCTYPE declaration at all, + then startDoctypeDeclHandler and endDoctypeDeclHandler will not + be called, despite an external subset being parsed. + Note: If XML_DTD is not defined when Expat is compiled, returns + XML_ERROR_FEATURE_REQUIRES_XML_DTD. + Note: If parser == NULL, returns XML_ERROR_INVALID_ARGUMENT. +*/ +XMLPARSEAPI(enum XML_Error) +XML_UseForeignDTD(XML_Parser parser, XML_Bool useDTD); + +/* Sets the base to be used for resolving relative URIs in system + identifiers in declarations. Resolving relative identifiers is + left to the application: this value will be passed through as the + base argument to the XML_ExternalEntityRefHandler, + XML_NotationDeclHandler and XML_UnparsedEntityDeclHandler. The base + argument will be copied. Returns XML_STATUS_ERROR if out of memory, + XML_STATUS_OK otherwise. +*/ +XMLPARSEAPI(enum XML_Status) +XML_SetBase(XML_Parser parser, const XML_Char *base); + +XMLPARSEAPI(const XML_Char *) +XML_GetBase(XML_Parser parser); + +/* Returns the number of the attribute/value pairs passed in last call + to the XML_StartElementHandler that were specified in the start-tag + rather than defaulted. Each attribute/value pair counts as 2; thus + this corresponds to an index into the atts array passed to the + XML_StartElementHandler. Returns -1 if parser == NULL. +*/ +XMLPARSEAPI(int) +XML_GetSpecifiedAttributeCount(XML_Parser parser); + +/* Returns the index of the ID attribute passed in the last call to + XML_StartElementHandler, or -1 if there is no ID attribute or + parser == NULL. Each attribute/value pair counts as 2; thus this + corresponds to an index into the atts array passed to the + XML_StartElementHandler. +*/ +XMLPARSEAPI(int) +XML_GetIdAttributeIndex(XML_Parser parser); + +# ifdef XML_ATTR_INFO +/* Source file byte offsets for the start and end of attribute names and values. + The value indices are exclusive of surrounding quotes; thus in a UTF-8 source + file an attribute value of "blah" will yield: + info->valueEnd - info->valueStart = 4 bytes. +*/ +typedef struct { + XML_Index nameStart; /* Offset to beginning of the attribute name. */ + XML_Index nameEnd; /* Offset after the attribute name's last byte. */ + XML_Index valueStart; /* Offset to beginning of the attribute value. */ + XML_Index valueEnd; /* Offset after the attribute value's last byte. */ +} XML_AttrInfo; + +/* Returns an array of XML_AttrInfo structures for the attribute/value pairs + passed in last call to the XML_StartElementHandler that were specified + in the start-tag rather than defaulted. Each attribute/value pair counts + as 1; thus the number of entries in the array is + XML_GetSpecifiedAttributeCount(parser) / 2. +*/ +XMLPARSEAPI(const XML_AttrInfo *) +XML_GetAttributeInfo(XML_Parser parser); +# endif + +/* Parses some input. Returns XML_STATUS_ERROR if a fatal error is + detected. The last call to XML_Parse must have isFinal true; len + may be zero for this call (or any other). + + Though the return values for these functions has always been + described as a Boolean value, the implementation, at least for the + 1.95.x series, has always returned exactly one of the XML_Status + values. +*/ +XMLPARSEAPI(enum XML_Status) +XML_Parse(XML_Parser parser, const char *s, int len, int isFinal); + +XMLPARSEAPI(void *) +XML_GetBuffer(XML_Parser parser, int len); + +XMLPARSEAPI(enum XML_Status) +XML_ParseBuffer(XML_Parser parser, int len, int isFinal); + +/* Stops parsing, causing XML_Parse() or XML_ParseBuffer() to return. + Must be called from within a call-back handler, except when aborting + (resumable = 0) an already suspended parser. Some call-backs may + still follow because they would otherwise get lost. Examples: + - endElementHandler() for empty elements when stopped in + startElementHandler(), + - endNameSpaceDeclHandler() when stopped in endElementHandler(), + and possibly others. + + Can be called from most handlers, including DTD related call-backs, + except when parsing an external parameter entity and resumable != 0. + Returns XML_STATUS_OK when successful, XML_STATUS_ERROR otherwise. + Possible error codes: + - XML_ERROR_SUSPENDED: when suspending an already suspended parser. + - XML_ERROR_FINISHED: when the parser has already finished. + - XML_ERROR_SUSPEND_PE: when suspending while parsing an external PE. + + When resumable != 0 (true) then parsing is suspended, that is, + XML_Parse() and XML_ParseBuffer() return XML_STATUS_SUSPENDED. + Otherwise, parsing is aborted, that is, XML_Parse() and XML_ParseBuffer() + return XML_STATUS_ERROR with error code XML_ERROR_ABORTED. + + *Note*: + This will be applied to the current parser instance only, that is, if + there is a parent parser then it will continue parsing when the + externalEntityRefHandler() returns. It is up to the implementation of + the externalEntityRefHandler() to call XML_StopParser() on the parent + parser (recursively), if one wants to stop parsing altogether. + + When suspended, parsing can be resumed by calling XML_ResumeParser(). +*/ +XMLPARSEAPI(enum XML_Status) +XML_StopParser(XML_Parser parser, XML_Bool resumable); + +/* Resumes parsing after it has been suspended with XML_StopParser(). + Must not be called from within a handler call-back. Returns same + status codes as XML_Parse() or XML_ParseBuffer(). + Additional error code XML_ERROR_NOT_SUSPENDED possible. + + *Note*: + This must be called on the most deeply nested child parser instance + first, and on its parent parser only after the child parser has finished, + to be applied recursively until the document entity's parser is restarted. + That is, the parent parser will not resume by itself and it is up to the + application to call XML_ResumeParser() on it at the appropriate moment. +*/ +XMLPARSEAPI(enum XML_Status) +XML_ResumeParser(XML_Parser parser); + +enum XML_Parsing { XML_INITIALIZED, XML_PARSING, XML_FINISHED, XML_SUSPENDED }; + +typedef struct { + enum XML_Parsing parsing; + XML_Bool finalBuffer; +} XML_ParsingStatus; + +/* Returns status of parser with respect to being initialized, parsing, + finished, or suspended and processing the final buffer. + XXX XML_Parse() and XML_ParseBuffer() should return XML_ParsingStatus, + XXX with XML_FINISHED_OK or XML_FINISHED_ERROR replacing XML_FINISHED +*/ +XMLPARSEAPI(void) +XML_GetParsingStatus(XML_Parser parser, XML_ParsingStatus *status); + +/* Creates an XML_Parser object that can parse an external general + entity; context is a '\0'-terminated string specifying the parse + context; encoding is a '\0'-terminated string giving the name of + the externally specified encoding, or NULL if there is no + externally specified encoding. The context string consists of a + sequence of tokens separated by formfeeds (\f); a token consisting + of a name specifies that the general entity of the name is open; a + token of the form prefix=uri specifies the namespace for a + particular prefix; a token of the form =uri specifies the default + namespace. This can be called at any point after the first call to + an ExternalEntityRefHandler so longer as the parser has not yet + been freed. The new parser is completely independent and may + safely be used in a separate thread. The handlers and userData are + initialized from the parser argument. Returns NULL if out of memory. + Otherwise returns a new XML_Parser object. +*/ +XMLPARSEAPI(XML_Parser) +XML_ExternalEntityParserCreate(XML_Parser parser, const XML_Char *context, + const XML_Char *encoding); + +enum XML_ParamEntityParsing { + XML_PARAM_ENTITY_PARSING_NEVER, + XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE, + XML_PARAM_ENTITY_PARSING_ALWAYS +}; + +/* Controls parsing of parameter entities (including the external DTD + subset). If parsing of parameter entities is enabled, then + references to external parameter entities (including the external + DTD subset) will be passed to the handler set with + XML_SetExternalEntityRefHandler. The context passed will be 0. + + Unlike external general entities, external parameter entities can + only be parsed synchronously. If the external parameter entity is + to be parsed, it must be parsed during the call to the external + entity ref handler: the complete sequence of + XML_ExternalEntityParserCreate, XML_Parse/XML_ParseBuffer and + XML_ParserFree calls must be made during this call. After + XML_ExternalEntityParserCreate has been called to create the parser + for the external parameter entity (context must be 0 for this + call), it is illegal to make any calls on the old parser until + XML_ParserFree has been called on the newly created parser. + If the library has been compiled without support for parameter + entity parsing (ie without XML_DTD being defined), then + XML_SetParamEntityParsing will return 0 if parsing of parameter + entities is requested; otherwise it will return non-zero. + Note: If XML_SetParamEntityParsing is called after XML_Parse or + XML_ParseBuffer, then it has no effect and will always return 0. + Note: If parser == NULL, the function will do nothing and return 0. +*/ +XMLPARSEAPI(int) +XML_SetParamEntityParsing(XML_Parser parser, + enum XML_ParamEntityParsing parsing); + +/* Sets the hash salt to use for internal hash calculations. + Helps in preventing DoS attacks based on predicting hash + function behavior. This must be called before parsing is started. + Returns 1 if successful, 0 when called after parsing has started. + Note: If parser == NULL, the function will do nothing and return 0. + DEPRECATED since Expat 2.8.0. + Please use XML_SetHashSalt16Bytes instead. +*/ +XML_ATTR_DEPRECATED("please use XML_SetHashSalt16Bytes instead") +XMLPARSEAPI(int) +XML_SetHashSalt(XML_Parser parser, unsigned long hash_salt); + +/* Sets the hash salt to use for internal hash calculations. + Helps in preventing DoS attacks based on predicting hash function behavior. + This must be called before parsing is started. + Returns XML_TRUE if successful, XML_FALSE when called after parsing has + started or when parser is NULL. + Added in Expat 2.8.0. +*/ +XMLPARSEAPI(XML_Bool) +XML_SetHashSalt16Bytes(XML_Parser parser, const uint8_t entropy[16]); + +/* If XML_Parse or XML_ParseBuffer have returned XML_STATUS_ERROR, then + XML_GetErrorCode returns information about the error. +*/ +XMLPARSEAPI(enum XML_Error) +XML_GetErrorCode(XML_Parser parser); + +/* These functions return information about the current parse + location. They may be called from any callback called to report + some parse event; in this case the location is the location of the + first of the sequence of characters that generated the event. When + called from callbacks generated by declarations in the document + prologue, the location identified isn't as neatly defined, but will + be within the relevant markup. When called outside of the callback + functions, the position indicated will be just past the last parse + event (regardless of whether there was an associated callback). + + They may also be called after returning from a call to XML_Parse + or XML_ParseBuffer. If the return value is XML_STATUS_ERROR then + the location is the location of the character at which the error + was detected; otherwise the location is the location of the last + parse event, as described above. + + Note: XML_GetCurrentLineNumber and XML_GetCurrentColumnNumber + return 0 to indicate an error. + Note: XML_GetCurrentByteIndex returns -1 to indicate an error. +*/ +XMLPARSEAPI(XML_Size) XML_GetCurrentLineNumber(XML_Parser parser); +XMLPARSEAPI(XML_Size) XML_GetCurrentColumnNumber(XML_Parser parser); +XMLPARSEAPI(XML_Index) XML_GetCurrentByteIndex(XML_Parser parser); + +/* Return the number of bytes in the current event. + Returns 0 if the event is in an internal entity. +*/ +XMLPARSEAPI(int) +XML_GetCurrentByteCount(XML_Parser parser); + +/* If XML_CONTEXT_BYTES is >=1, returns the input buffer, sets + the integer pointed to by offset to the offset within this buffer + of the current parse position, and sets the integer pointed to by size + to the size of this buffer (the number of input bytes). Otherwise + returns a NULL pointer. Also returns a NULL pointer if a parse isn't + active. + + NOTE: The character pointer returned should not be used outside + the handler that makes the call. +*/ +XMLPARSEAPI(const char *) +XML_GetInputContext(XML_Parser parser, int *offset, int *size); + +/* For backwards compatibility with previous versions. */ +# define XML_GetErrorLineNumber XML_GetCurrentLineNumber +# define XML_GetErrorColumnNumber XML_GetCurrentColumnNumber +# define XML_GetErrorByteIndex XML_GetCurrentByteIndex + +/* Frees the content model passed to the element declaration handler */ +XMLPARSEAPI(void) +XML_FreeContentModel(XML_Parser parser, XML_Content *model); + +/* Exposing the memory handling functions used in Expat */ +XMLPARSEAPI(void *) +XML_ATTR_MALLOC +XML_ATTR_ALLOC_SIZE(2) +XML_MemMalloc(XML_Parser parser, size_t size); + +XMLPARSEAPI(void *) +XML_ATTR_ALLOC_SIZE(3) +XML_MemRealloc(XML_Parser parser, void *ptr, size_t size); + +XMLPARSEAPI(void) +XML_MemFree(XML_Parser parser, void *ptr); + +/* Frees memory used by the parser. */ +XMLPARSEAPI(void) +XML_ParserFree(XML_Parser parser); + +/* Returns a string describing the error. */ +XMLPARSEAPI(const XML_LChar *) +XML_ErrorString(enum XML_Error code); + +/* Return a string containing the version number of this expat */ +XMLPARSEAPI(const XML_LChar *) +XML_ExpatVersion(void); + +typedef struct { + int major; + int minor; + int micro; +} XML_Expat_Version; + +/* Return an XML_Expat_Version structure containing numeric version + number information for this version of expat. +*/ +XMLPARSEAPI(XML_Expat_Version) +XML_ExpatVersionInfo(void); + +/* Added in Expat 1.95.5. */ +enum XML_FeatureEnum { + XML_FEATURE_END = 0, + XML_FEATURE_UNICODE, + XML_FEATURE_UNICODE_WCHAR_T, + XML_FEATURE_DTD, + XML_FEATURE_CONTEXT_BYTES, + XML_FEATURE_MIN_SIZE, + XML_FEATURE_SIZEOF_XML_CHAR, + XML_FEATURE_SIZEOF_XML_LCHAR, + /* Added in Expat 2.0.0. */ + XML_FEATURE_NS, + /* Added in Expat 2.0.1. */ + XML_FEATURE_LARGE_SIZE, + /* Added in Expat 2.1.0. */ + XML_FEATURE_ATTR_INFO, + /* Added in Expat 2.4.0. */ + XML_FEATURE_BILLION_LAUGHS_ATTACK_PROTECTION_MAXIMUM_AMPLIFICATION_DEFAULT, + XML_FEATURE_BILLION_LAUGHS_ATTACK_PROTECTION_ACTIVATION_THRESHOLD_DEFAULT, + /* Added in Expat 2.6.0. */ + XML_FEATURE_GE, + /* Added in Expat 2.7.2. */ + XML_FEATURE_ALLOC_TRACKER_MAXIMUM_AMPLIFICATION_DEFAULT, + XML_FEATURE_ALLOC_TRACKER_ACTIVATION_THRESHOLD_DEFAULT, + /* Additional features must be added to the end of this enum. */ +}; + +typedef struct { + enum XML_FeatureEnum feature; + const XML_LChar *name; + long int value; +} XML_Feature; + +XMLPARSEAPI(const XML_Feature *) +XML_GetFeatureList(void); + +# if defined(XML_DTD) || (defined(XML_GE) && XML_GE == 1) +/* Added in Expat 2.4.0 for XML_DTD defined and + * added in Expat 2.6.0 for XML_GE == 1. */ +XMLPARSEAPI(XML_Bool) +XML_SetBillionLaughsAttackProtectionMaximumAmplification( + XML_Parser parser, float maximumAmplificationFactor); + +/* Added in Expat 2.4.0 for XML_DTD defined and + * added in Expat 2.6.0 for XML_GE == 1. */ +XMLPARSEAPI(XML_Bool) +XML_SetBillionLaughsAttackProtectionActivationThreshold( + XML_Parser parser, unsigned long long activationThresholdBytes); + +/* Added in Expat 2.7.2. */ +XMLPARSEAPI(XML_Bool) +XML_SetAllocTrackerMaximumAmplification(XML_Parser parser, + float maximumAmplificationFactor); + +/* Added in Expat 2.7.2. */ +XMLPARSEAPI(XML_Bool) +XML_SetAllocTrackerActivationThreshold( + XML_Parser parser, unsigned long long activationThresholdBytes); +# endif + +/* Added in Expat 2.6.0. */ +XMLPARSEAPI(XML_Bool) +XML_SetReparseDeferralEnabled(XML_Parser parser, XML_Bool enabled); + +/* Expat follows the semantic versioning convention. + See https://semver.org +*/ +# define XML_MAJOR_VERSION 2 +# define XML_MINOR_VERSION 8 +# define XML_MICRO_VERSION 5 + +# ifdef __cplusplus +} +# endif + +#endif /* not Expat_INCLUDED */ diff --git a/port/third_party/expat/expat_config.h b/port/third_party/expat/expat_config.h new file mode 100644 index 00000000..a95027d3 --- /dev/null +++ b/port/third_party/expat/expat_config.h @@ -0,0 +1,23 @@ +/* Expat's build configuration for the ports (README.md): the menus' XML +files only, so no DTDs, no namespaces and no general entities. */ + +#ifndef EXPAT_CONFIG_H +#define EXPAT_CONFIG_H 1 + +#define BYTEORDER 1234 +#define HAVE_MEMMOVE 1 +#define HAVE_STDINT_H 1 +#define HAVE_STDLIB_H 1 +#define HAVE_STRING_H 1 +#define STDC_HEADERS 1 + +#define XML_CONTEXT_BYTES 1024 +#define XML_GE 0 + +/* The hash salt only guards against hash flooding, which small local files +can't do; Windows takes it from rand_s, and the others from the time */ +#ifndef _WIN32 +#define XML_POOR_ENTROPY 1 +#endif + +#endif diff --git a/port/third_party/expat/expat_external.h b/port/third_party/expat/expat_external.h new file mode 100644 index 00000000..cf80f960 --- /dev/null +++ b/port/third_party/expat/expat_external.h @@ -0,0 +1,180 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2000-2004 Fred L. Drake, Jr. + Copyright (c) 2001-2002 Greg Stein + Copyright (c) 2002-2006 Karl Waclawek + Copyright (c) 2016 Cristian Rodríguez + Copyright (c) 2016-2025 Sebastian Pipping + Copyright (c) 2017 Rhodri James + Copyright (c) 2018 Yury Gribov + Copyright (c) 2026 Matthew Fernandez + Copyright (c) 2026 Braian Plaku + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#ifndef Expat_External_INCLUDED +# define Expat_External_INCLUDED 1 + +/* External API definitions */ + +/* Expat tries very hard to make the API boundary very specifically + defined. There are two macros defined to control this boundary; + each of these can be defined before including this header to + achieve some different behavior, but doing so is not recommended or + tested frequently. + + XMLCALL - The calling convention to use for all calls across the + "library boundary." This will default to cdecl, and + try really hard to tell the compiler that's what we + want. + + XMLIMPORT - Whatever magic is needed to note that a function is + to be imported from a dynamically loaded library + (.dll, .so, or .sl, depending on your platform). + + The XMLCALL macro was added in Expat 1.95.7. The only one which is + expected to be directly useful in client code is XMLCALL. + + Note that on at least some Unix versions, the Expat library must be + compiled with the cdecl calling convention as the default since + system headers may assume the cdecl convention. +*/ +# ifndef XMLCALL +# if defined(_MSC_VER) +# define XMLCALL __cdecl +# elif defined(__GNUC__) && defined(__i386) && ! defined(__INTEL_COMPILER) +# define XMLCALL __attribute__((cdecl)) +# else +/* For any platform which uses this definition and supports more than + one calling convention, we need to extend this definition to + declare the convention used on that platform, if it's possible to + do so. + + If this is the case for your platform, please file a bug report + with information on how to identify your platform via the C + pre-processor and how to specify the same calling convention as the + platform's malloc() implementation. +*/ +# define XMLCALL +# endif +# endif /* not defined XMLCALL */ + +# if ! defined(XML_STATIC) && ! defined(XMLIMPORT) +# ifndef XML_BUILDING_EXPAT +/* using Expat from an application */ + +# if defined(_MSC_VER) && ! defined(__BEOS__) && ! defined(__CYGWIN__) +# define XMLIMPORT __declspec(dllimport) +# endif + +# endif +# endif /* not defined XML_STATIC */ + +# ifndef XML_ENABLE_VISIBILITY +# define XML_ENABLE_VISIBILITY 0 +# endif + +# if ! defined(XMLIMPORT) && XML_ENABLE_VISIBILITY +# define XMLIMPORT __attribute__((visibility("default"))) +# endif + +/* If we didn't define it above, define it away: */ +# ifndef XMLIMPORT +# define XMLIMPORT +# endif + +# if defined(__GNUC__) \ + && (__GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 96)) +# define XML_ATTR_MALLOC __attribute__((__malloc__)) +# else +# define XML_ATTR_MALLOC +# endif + +# if defined(__GNUC__) \ + && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)) +# define XML_ATTR_ALLOC_SIZE(x) __attribute__((__alloc_size__(x))) +# else +# define XML_ATTR_ALLOC_SIZE(x) +# endif + +/* Marks a function that Expat still provides but that callers should move off + of. */ +# if defined(__clang__) || defined(__GNUC__) +# define XML_ATTR_DEPRECATED(message) \ + __attribute__((__deprecated__(message))) +# elif defined(_MSC_VER) +# define XML_ATTR_DEPRECATED(message) __declspec(deprecated(message)) +# else +# define XML_ATTR_DEPRECATED(message) // empty i.e. no deprecation +# endif + +# define XMLPARSEAPI(type) XMLIMPORT type XMLCALL + +# ifdef __cplusplus +extern "C" { +# endif + +# ifdef XML_UNICODE_WCHAR_T +# ifndef XML_UNICODE +# define XML_UNICODE +# endif +# if defined(__SIZEOF_WCHAR_T__) && (__SIZEOF_WCHAR_T__ != 2) +# error "sizeof(wchar_t) != 2; Need -fshort-wchar for both Expat and libc" +# endif +# endif + +# ifdef XML_UNICODE /* Information is UTF-16 encoded. */ +# ifdef XML_UNICODE_WCHAR_T +typedef wchar_t XML_Char; +typedef wchar_t XML_LChar; +# else +typedef unsigned short XML_Char; +typedef char XML_LChar; +# endif /* XML_UNICODE_WCHAR_T */ +# else /* Information is UTF-8 encoded. */ +typedef char XML_Char; +typedef char XML_LChar; +# endif /* XML_UNICODE */ + +# ifdef XML_LARGE_SIZE /* Use large integers for file/stream positions. */ +typedef long long XML_Index; +typedef unsigned long long XML_Size; +# else +typedef long XML_Index; +typedef unsigned long XML_Size; +# endif /* XML_LARGE_SIZE */ + +# ifdef __cplusplus +} +# endif + +#endif /* not Expat_External_INCLUDED */ diff --git a/port/third_party/expat/fallthrough.h b/port/third_party/expat/fallthrough.h new file mode 100644 index 00000000..0152d1bd --- /dev/null +++ b/port/third_party/expat/fallthrough.h @@ -0,0 +1,51 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2026 Nick Begg + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#ifndef FALLTHROUGH_H +# define FALLTHROUGH_H 1 + +// Explicit fallthrough in switch case to avoid warnings +// with compiler flag -Wimplicit-fallthrough. + +# define EXPAT_FALLTHROUGH \ + do { \ + } while (0) + +# if defined(__has_attribute) +# if __has_attribute(fallthrough) +# undef EXPAT_FALLTHROUGH +# define EXPAT_FALLTHROUGH __attribute__((fallthrough)) +# endif +# endif + +#endif // FALLTHROUGH_H diff --git a/port/third_party/expat/hash_table.h b/port/third_party/expat/hash_table.h new file mode 100644 index 00000000..f290aad1 --- /dev/null +++ b/port/third_party/expat/hash_table.h @@ -0,0 +1,78 @@ +/* Hash table related internal API + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2026 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#if ! defined(HASH_TABLE_H) +# define HASH_TABLE_H 1 + +# include "expat.h" // for XML_Bool, XML_Parser +# include "internal.h" // for XML_NONTESTING_STATIC + +# include // for size_t + +typedef const XML_Char *KEY; + +typedef struct { + KEY name; +} NAMED; + +typedef struct { + NAMED **v; + unsigned char power; + size_t size; + size_t used; + XML_Parser parser; +} HASH_TABLE; + +typedef struct { + NAMED **p; + NAMED **end; +} HASH_TABLE_ITER; + +XML_NONTESTING_STATIC NAMED *lookupWithLength(XML_Parser parser, + HASH_TABLE *table, KEY name, + size_t nameLen, + size_t createSize); +XML_NONTESTING_STATIC NAMED *lookup(XML_Parser parser, HASH_TABLE *table, + KEY name, size_t createSize); + +XML_NONTESTING_STATIC void hashTableInit(HASH_TABLE *table, XML_Parser parser); +XML_NONTESTING_STATIC void hashTableClear(HASH_TABLE *table); +XML_NONTESTING_STATIC void hashTableDestroy(HASH_TABLE *table); +XML_NONTESTING_STATIC void hashTableIterInit(HASH_TABLE_ITER *iter, + const HASH_TABLE *table); +XML_NONTESTING_STATIC NAMED *hashTableIterNext(HASH_TABLE_ITER *iter); + +XML_NONTESTING_STATIC XML_Bool keyeq(KEY s1, size_t s1len, KEY s2); +XML_NONTESTING_STATIC size_t keylen(KEY s); + +#endif // ! defined(HASH_TABLE_H) diff --git a/port/third_party/expat/iasciitab.h b/port/third_party/expat/iasciitab.h new file mode 100644 index 00000000..1de8d519 --- /dev/null +++ b/port/third_party/expat/iasciitab.h @@ -0,0 +1,69 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2017 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +/* Like asciitab.h, except that 0xD has code BT_S rather than BT_CR */ +/* 0x00 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x04 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x08 */ BT_NONXML, BT_S, BT_LF, BT_NONXML, + /* 0x0C */ BT_NONXML, BT_S, BT_NONXML, BT_NONXML, + /* 0x10 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x14 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x18 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x1C */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0x20 */ BT_S, BT_EXCL, BT_QUOT, BT_NUM, + /* 0x24 */ BT_OTHER, BT_PERCNT, BT_AMP, BT_APOS, + /* 0x28 */ BT_LPAR, BT_RPAR, BT_AST, BT_PLUS, + /* 0x2C */ BT_COMMA, BT_MINUS, BT_NAME, BT_SOL, + /* 0x30 */ BT_DIGIT, BT_DIGIT, BT_DIGIT, BT_DIGIT, + /* 0x34 */ BT_DIGIT, BT_DIGIT, BT_DIGIT, BT_DIGIT, + /* 0x38 */ BT_DIGIT, BT_DIGIT, BT_COLON, BT_SEMI, + /* 0x3C */ BT_LT, BT_EQUALS, BT_GT, BT_QUEST, + /* 0x40 */ BT_OTHER, BT_HEX, BT_HEX, BT_HEX, + /* 0x44 */ BT_HEX, BT_HEX, BT_HEX, BT_NMSTRT, + /* 0x48 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x4C */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x50 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x54 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x58 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_LSQB, + /* 0x5C */ BT_OTHER, BT_RSQB, BT_OTHER, BT_NMSTRT, + /* 0x60 */ BT_OTHER, BT_HEX, BT_HEX, BT_HEX, + /* 0x64 */ BT_HEX, BT_HEX, BT_HEX, BT_NMSTRT, + /* 0x68 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x6C */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x70 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x74 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0x78 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_OTHER, + /* 0x7C */ BT_VERBAR, BT_OTHER, BT_OTHER, BT_OTHER, diff --git a/port/third_party/expat/internal.h b/port/third_party/expat/internal.h new file mode 100644 index 00000000..86655371 --- /dev/null +++ b/port/third_party/expat/internal.h @@ -0,0 +1,156 @@ +/* internal.h + + Internal definitions used by Expat. This is not needed to compile + client code. + + The following calling convention macros are defined for frequently + called functions: + + inline - Used for selected internal functions for which inlining + may improve performance on some platforms. + + Note: Use of these macros is based on judgement, not hard rules, + and therefore subject to change. + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2002-2003 Fred L. Drake, Jr. + Copyright (c) 2002-2006 Karl Waclawek + Copyright (c) 2003 Greg Stein + Copyright (c) 2016-2026 Sebastian Pipping + Copyright (c) 2018 Yury Gribov + Copyright (c) 2019 David Loffredo + Copyright (c) 2023-2024 Sony Corporation / Snild Dolkow + Copyright (c) 2024 Taichi Haradaguchi <20001722@ymail.ne.jp> + Copyright (c) 2026 Matthew Wozniczka + Copyright (c) 2026 Braian Plaku + Copyright (c) 2026 Florian Schmaus + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#if ! defined(INTERNAL_H) +# define INTERNAL_H 1 + +# ifndef XML_MIN_SIZE +# if ! defined(inline) +# ifdef __GNUC__ +# define inline __inline +# endif /* __GNUC__ */ +# endif +# endif /* XML_MIN_SIZE */ + +# ifndef inline +# define inline +# endif + +# if ! defined(XML_NONTESTING_STATIC) +# if defined(XML_TESTING) +# define XML_NONTESTING_STATIC // empty i.e. not static +# else +# define XML_NONTESTING_STATIC static +# endif +# endif + +# include // ULONG_MAX +# include // size_t + +# if defined(_WIN32) \ + && (! defined(__USE_MINGW_ANSI_STDIO) \ + || (1 - __USE_MINGW_ANSI_STDIO - 1 == 0)) +# define EXPAT_FMT_LLX(midpart) "%" midpart "I64x" +# define EXPAT_FMT_ULL(midpart) "%" midpart "I64u" +# if defined(_WIN64) // Note: modifiers "td" and "zu" do not work for MinGW +# define EXPAT_FMT_PTRDIFF_T(midpart) "%" midpart "I64d" +# define EXPAT_FMT_SIZE_T(midpart) "%" midpart "I64u" +# else +# define EXPAT_FMT_PTRDIFF_T(midpart) "%" midpart "d" +# define EXPAT_FMT_SIZE_T(midpart) "%" midpart "u" +# endif +# else +# include // PRIdPTR, PRIuPTR +# define EXPAT_FMT_LLX(midpart) "%" midpart "llx" +# define EXPAT_FMT_ULL(midpart) "%" midpart "llu" +# define EXPAT_FMT_PTRDIFF_T(midpart) "%" midpart PRIdPTR +# define EXPAT_FMT_SIZE_T(midpart) "%" midpart PRIuPTR +# endif + +# ifndef UNUSED_P +# define UNUSED_P(p) (void)p +# endif + +/* NOTE BEGIN If you ever patch these defaults to greater values + for non-attack XML payload in your environment, + please file a bug report with libexpat. Thank you! +*/ +# define EXPAT_BILLION_LAUGHS_ATTACK_PROTECTION_MAXIMUM_AMPLIFICATION_DEFAULT \ + 100.0f +# define EXPAT_BILLION_LAUGHS_ATTACK_PROTECTION_ACTIVATION_THRESHOLD_DEFAULT \ + 8388608 // 8 MiB, 2^23 + +# define EXPAT_ALLOC_TRACKER_MAXIMUM_AMPLIFICATION_DEFAULT 100.0f +# define EXPAT_ALLOC_TRACKER_ACTIVATION_THRESHOLD_DEFAULT \ + 67108864 // 64 MiB, 2^26 + +// NOTE: If function expat_alloc was user facing, EXPAT_MALLOC_ALIGNMENT would +// have to take sizeof(long double) into account +union expat_align { + long long l; + void *p; +}; +# define EXPAT_MALLOC_ALIGNMENT sizeof(union expat_align) +# define EXPAT_MALLOC_PADDING ((EXPAT_MALLOC_ALIGNMENT) - sizeof(size_t)) + +/* NOTE END */ + +# include "expat.h" // so we can use type XML_Parser below + +void _INTERNAL_trim_to_complete_utf8_characters(const char *from, + const char **fromLimRef); + +# if defined(XML_GE) && XML_GE == 1 +unsigned long long testingAccountingGetCountBytesDirect(XML_Parser parser); +unsigned long long testingAccountingGetCountBytesIndirect(XML_Parser parser); +const char *unsignedCharToPrintable(unsigned char c); +# endif + +extern +# if ! defined(XML_TESTING) + const +# endif + XML_Bool g_reparseDeferralEnabledDefault; // written ONLY in runtests.c +# if defined(XML_TESTING) + +int xmlSetHashSalt(XML_Parser parser, unsigned long hash_salt); + +void *expat_malloc(XML_Parser parser, size_t size, int sourceLine); +void expat_free(XML_Parser parser, void *ptr, int sourceLine); +void *expat_realloc(XML_Parser parser, void *ptr, size_t size, int sourceLine); +extern unsigned int g_bytesScanned; // used for testing only +# endif + +#endif // not defined INTERNAL_H diff --git a/port/third_party/expat/latin1tab.h b/port/third_party/expat/latin1tab.h new file mode 100644 index 00000000..3793f4f3 --- /dev/null +++ b/port/third_party/expat/latin1tab.h @@ -0,0 +1,68 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2017 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +/* 0x80 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0x84 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0x88 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0x8C */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0x90 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0x94 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0x98 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0x9C */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0xA0 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0xA4 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0xA8 */ BT_OTHER, BT_OTHER, BT_NMSTRT, BT_OTHER, + /* 0xAC */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0xB0 */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0xB4 */ BT_OTHER, BT_NMSTRT, BT_OTHER, BT_NAME, + /* 0xB8 */ BT_OTHER, BT_OTHER, BT_NMSTRT, BT_OTHER, + /* 0xBC */ BT_OTHER, BT_OTHER, BT_OTHER, BT_OTHER, + /* 0xC0 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xC4 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xC8 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xCC */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xD0 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xD4 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_OTHER, + /* 0xD8 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xDC */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xE0 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xE4 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xE8 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xEC */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xF0 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xF4 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_OTHER, + /* 0xF8 */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, + /* 0xFC */ BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, BT_NMSTRT, diff --git a/port/third_party/expat/memory_sanitizer.h b/port/third_party/expat/memory_sanitizer.h new file mode 100644 index 00000000..f739b88f --- /dev/null +++ b/port/third_party/expat/memory_sanitizer.h @@ -0,0 +1,53 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2026 Matthew Fernandez + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#if ! defined(MEMORY_SANITIZER_H) +# define MEMORY_SANITIZER_H 1 + +# if defined(__has_feature) +# if __has_feature(memory_sanitizer) +# include + +// inform Memory Sanitizer that [base, base + extent) is now initialized +# define MSAN_UNPOISON(base, extent) __msan_unpoison((base), (extent)) + +# endif +# endif + +# if ! defined(MSAN_UNPOISON) +# define MSAN_UNPOISON(base, extent) \ + do { \ + } while (0) +# endif + +#endif // ! defined(MEMORY_SANITIZER_H) diff --git a/port/third_party/expat/nametab.h b/port/third_party/expat/nametab.h new file mode 100644 index 00000000..23858515 --- /dev/null +++ b/port/third_party/expat/nametab.h @@ -0,0 +1,138 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2000 Clark Cooper + Copyright (c) 2017 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +static const unsigned namingBitmap[] = { + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, + 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x00000000, 0x04000000, + 0x87FFFFFE, 0x07FFFFFE, 0x00000000, 0x00000000, 0xFF7FFFFF, 0xFF7FFFFF, + 0xFFFFFFFF, 0x7FF3FFFF, 0xFFFFFDFE, 0x7FFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, + 0xFFFFE00F, 0xFC31FFFF, 0x00FFFFFF, 0x00000000, 0xFFFF0000, 0xFFFFFFFF, + 0xFFFFFFFF, 0xF80001FF, 0x00000003, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0xFFFFD740, 0xFFFFFFFB, 0x547F7FFF, 0x000FFFFD, + 0xFFFFDFFE, 0xFFFFFFFF, 0xDFFEFFFF, 0xFFFFFFFF, 0xFFFF0003, 0xFFFFFFFF, + 0xFFFF199F, 0x033FCFFF, 0x00000000, 0xFFFE0000, 0x027FFFFF, 0xFFFFFFFE, + 0x0000007F, 0x00000000, 0xFFFF0000, 0x000707FF, 0x00000000, 0x07FFFFFE, + 0x000007FE, 0xFFFE0000, 0xFFFFFFFF, 0x7CFFFFFF, 0x002F7FFF, 0x00000060, + 0xFFFFFFE0, 0x23FFFFFF, 0xFF000000, 0x00000003, 0xFFF99FE0, 0x03C5FDFF, + 0xB0000000, 0x00030003, 0xFFF987E0, 0x036DFDFF, 0x5E000000, 0x001C0000, + 0xFFFBAFE0, 0x23EDFDFF, 0x00000000, 0x00000001, 0xFFF99FE0, 0x23CDFDFF, + 0xB0000000, 0x00000003, 0xD63DC7E0, 0x03BFC718, 0x00000000, 0x00000000, + 0xFFFDDFE0, 0x03EFFDFF, 0x00000000, 0x00000003, 0xFFFDDFE0, 0x03EFFDFF, + 0x40000000, 0x00000003, 0xFFFDDFE0, 0x03FFFDFF, 0x00000000, 0x00000003, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xFFFFFFFE, 0x000D7FFF, + 0x0000003F, 0x00000000, 0xFEF02596, 0x200D6CAE, 0x0000001F, 0x00000000, + 0x00000000, 0x00000000, 0xFFFFFEFF, 0x000003FF, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0xFFFFFFFF, 0xFFFF003F, 0x007FFFFF, 0x0007DAED, 0x50000000, + 0x82315001, 0x002C62AB, 0x40000000, 0xF580C900, 0x00000007, 0x02010800, + 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x0FFFFFFF, 0xFFFFFFFF, + 0xFFFFFFFF, 0x03FFFFFF, 0x3F3FFFFF, 0xFFFFFFFF, 0xAAFF3F3F, 0x3FFFFFFF, + 0xFFFFFFFF, 0x5FDFFFFF, 0x0FCF1FDC, 0x1FDC1FFF, 0x00000000, 0x00004C40, + 0x00000000, 0x00000000, 0x00000007, 0x00000000, 0x00000000, 0x00000000, + 0x00000080, 0x000003FE, 0xFFFFFFFE, 0xFFFFFFFF, 0x001FFFFF, 0xFFFFFFFE, + 0xFFFFFFFF, 0x07FFFFFF, 0xFFFFFFE0, 0x00001FFF, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xFFFFFFFF, 0xFFFFFFFF, + 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x0000003F, 0x00000000, 0x00000000, + 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x0000000F, + 0x00000000, 0x00000000, 0x00000000, 0x07FF6000, 0x87FFFFFE, 0x07FFFFFE, + 0x00000000, 0x00800000, 0xFF7FFFFF, 0xFF7FFFFF, 0x00FFFFFF, 0x00000000, + 0xFFFF0000, 0xFFFFFFFF, 0xFFFFFFFF, 0xF80001FF, 0x00030003, 0x00000000, + 0xFFFFFFFF, 0xFFFFFFFF, 0x0000003F, 0x00000003, 0xFFFFD7C0, 0xFFFFFFFB, + 0x547F7FFF, 0x000FFFFD, 0xFFFFDFFE, 0xFFFFFFFF, 0xDFFEFFFF, 0xFFFFFFFF, + 0xFFFF007B, 0xFFFFFFFF, 0xFFFF199F, 0x033FCFFF, 0x00000000, 0xFFFE0000, + 0x027FFFFF, 0xFFFFFFFE, 0xFFFE007F, 0xBBFFFFFB, 0xFFFF0016, 0x000707FF, + 0x00000000, 0x07FFFFFE, 0x0007FFFF, 0xFFFF03FF, 0xFFFFFFFF, 0x7CFFFFFF, + 0xFFEF7FFF, 0x03FF3DFF, 0xFFFFFFEE, 0xF3FFFFFF, 0xFF1E3FFF, 0x0000FFCF, + 0xFFF99FEE, 0xD3C5FDFF, 0xB080399F, 0x0003FFCF, 0xFFF987E4, 0xD36DFDFF, + 0x5E003987, 0x001FFFC0, 0xFFFBAFEE, 0xF3EDFDFF, 0x00003BBF, 0x0000FFC1, + 0xFFF99FEE, 0xF3CDFDFF, 0xB0C0398F, 0x0000FFC3, 0xD63DC7EC, 0xC3BFC718, + 0x00803DC7, 0x0000FF80, 0xFFFDDFEE, 0xC3EFFDFF, 0x00603DDF, 0x0000FFC3, + 0xFFFDDFEC, 0xC3EFFDFF, 0x40603DDF, 0x0000FFC3, 0xFFFDDFEC, 0xC3FFFDFF, + 0x00803DCF, 0x0000FFC3, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0xFFFFFFFE, 0x07FF7FFF, 0x03FF7FFF, 0x00000000, 0xFEF02596, 0x3BFF6CAE, + 0x03FF3F5F, 0x00000000, 0x03000000, 0xC2A003FF, 0xFFFFFEFF, 0xFFFE03FF, + 0xFEBF0FDF, 0x02FE3FFF, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x1FFF0000, 0x00000002, + 0x000000A0, 0x003EFFFE, 0xFFFFFFFE, 0xFFFFFFFF, 0x661FFFFF, 0xFFFFFFFE, + 0xFFFFFFFF, 0x77FFFFFF, +}; +static const unsigned char nmstrtPages[] = { + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x09, 0x0A, 0x0B, + 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x13, 0x00, 0x14, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x15, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; +static const unsigned char namePages[] = { + 0x19, 0x03, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x00, 0x00, 0x1F, 0x20, 0x21, + 0x22, 0x23, 0x24, 0x25, 0x10, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x13, 0x26, 0x14, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x27, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; diff --git a/port/third_party/expat/random_rand_s.c b/port/third_party/expat/random_rand_s.c new file mode 100644 index 00000000..4001ce6c --- /dev/null +++ b/port/third_party/expat/random_rand_s.c @@ -0,0 +1,90 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2019 David Loffredo + Copyright (c) 2019-2026 Sebastian Pipping + Copyright (c) 2019 Ben Wagner + Copyright (c) 2019 Vadim Zeitlin + Copyright (c) 2026 Matthew Fernandez + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#include "random_rand_s.h" + +/* force stdlib to define rand_s() */ +#if ! defined(_CRT_RAND_S) +# define _CRT_RAND_S +#endif + +// Workaround MinGW GCC trouble with recognizing `rand_s`, likely related +// to return type `error_t`; the symptom was: +// > error: implicit declaration of function ‘rand_s’ +#if defined(__MINGW32__) +# include +#endif + +#include // for rand_s +#include // for memcpy + +// Help clang-tidy out with prototype of function `rand_s` +#if defined(XML_CLANG_TIDY) +int rand_s(unsigned int *); +#endif + +/* Provide declaration of rand_s() for MinGW-32 (not 64, which has it), + as it didn't declare it in its header prior to version 5.3.0 of its + runtime package (mingwrt, containing stdlib.h). The upstream fix + was introduced at https://osdn.net/projects/mingw/ticket/39658 . */ +#if defined(__MINGW32__) && defined(__MINGW32_VERSION) \ + && __MINGW32_VERSION < 5003000L && ! defined(__MINGW64_VERSION_MAJOR) +__declspec(dllimport) int rand_s(unsigned int *); +#endif + +/* Obtain entropy on Windows using the rand_s() function which + * generates cryptographically secure random numbers. Internally it + * uses RtlGenRandom API which is present in Windows XP and later. + */ +bool +writeRandomBytes_rand_s(void *target, size_t count) { + size_t bytesWrittenTotal = 0; + + while (bytesWrittenTotal < count) { + unsigned int random32 = 0; + + if (rand_s(&random32)) + return false; /* failure */ + + size_t toUse = count - bytesWrittenTotal; + if (toUse > sizeof(random32)) + toUse = sizeof(random32); + memcpy((char *)target + bytesWrittenTotal, &random32, toUse); + bytesWrittenTotal += toUse; + } + return true; /* success */ +} diff --git a/port/third_party/expat/random_rand_s.h b/port/third_party/expat/random_rand_s.h new file mode 100644 index 00000000..461fc608 --- /dev/null +++ b/port/third_party/expat/random_rand_s.h @@ -0,0 +1,43 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2019 David Loffredo + Copyright (c) 2026 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#if ! defined(RANDOM_RAND_S_H) +# define RANDOM_RAND_S_H 1 + +# include +# include // for size_t + +bool writeRandomBytes_rand_s(void *target, size_t count); + +#endif // ! defined(RANDOM_RAND_S_H) diff --git a/port/third_party/expat/siphash.h b/port/third_party/expat/siphash.h new file mode 100644 index 00000000..ac1fbfaf --- /dev/null +++ b/port/third_party/expat/siphash.h @@ -0,0 +1,395 @@ +/* ========================================================================== + * siphash.h - SipHash-2-4 in a single header file + * -------------------------------------------------------------------------- + * Derived by William Ahern from the reference implementation[1] published[2] + * by Jean-Philippe Aumasson and Daniel J. Berstein. + * Minimal changes by Sebastian Pipping and Victor Stinner on top, see below. + * Licensed under the CC0 Public Domain Dedication license. + * + * 1. https://www.131002.net/siphash/siphash24.c + * 2. https://www.131002.net/siphash/ + * + * SPDX-License-Identifier: CC0-1.0 + * -------------------------------------------------------------------------- + * HISTORY: + * + * 2020-10-03 (Sebastian Pipping) + * - Drop support for Visual Studio 9.0/2008 and earlier + * + * 2019-08-03 (Sebastian Pipping) + * - Mark part of sip24_valid as to be excluded from clang-format + * - Re-format code using clang-format 9 + * + * 2018-07-08 (Anton Maklakov) + * - Add "fall through" markers for GCC's -Wimplicit-fallthrough + * + * 2017-11-03 (Sebastian Pipping) + * - Hide sip_tobin and sip_binof unless SIPHASH_TOBIN macro is defined + * + * 2017-07-25 (Vadim Zeitlin) + * - Fix use of SIPHASH_MAIN macro + * + * 2017-07-05 (Sebastian Pipping) + * - Use _SIP_ULL macro to not require a C++11 compiler if compiled as C++ + * - Add const qualifiers at two places + * - Ensure <=80 characters line length (assuming tab width 4) + * + * 2017-06-23 (Victor Stinner) + * - Address Win64 compile warnings + * + * 2017-06-18 (Sebastian Pipping) + * - Clarify license note in the header + * - Address C89 issues: + * - Stop using inline keyword (and let compiler decide) + * - Replace _Bool by int + * - Turn macro siphash24 into a function + * - Address invalid conversion (void pointer) by explicit cast + * - Address lack of stdint.h for Visual Studio 2003 to 2008 + * - Always expose sip24_valid (for self-tests) + * + * 2012-11-04 - Born. (William Ahern) + * -------------------------------------------------------------------------- + * USAGE: + * + * SipHash-2-4 takes as input two 64-bit words as the key, some number of + * message bytes, and outputs a 64-bit word as the message digest. This + * implementation employs two data structures: a struct sipkey for + * representing the key, and a struct siphash for representing the hash + * state. + * + * For converting a 16-byte unsigned char array to a key, use either the + * macro sip_keyof or the routine sip_tokey. The former instantiates a + * compound literal key, while the latter requires a key object as a + * parameter. + * + * unsigned char secret[16]; + * arc4random_buf(secret, sizeof secret); + * struct sipkey *key = sip_keyof(secret); + * + * For hashing a message, use either the convenience macro siphash24 or the + * routines sip24_init, sip24_update, and sip24_final. + * + * struct siphash state; + * void *msg; + * size_t len; + * uint64_t hash; + * + * sip24_init(&state, key); + * sip24_update(&state, msg, len); + * hash = sip24_final(&state); + * + * or + * + * hash = siphash24(msg, len, key); + * + * To convert the 64-bit hash value to a canonical 8-byte little-endian + * binary representation, use either the macro sip_binof or the routine + * sip_tobin. The former instantiates and returns a compound literal array, + * while the latter requires an array object as a parameter. + * -------------------------------------------------------------------------- + * NOTES: + * + * o Neither sip_keyof, sip_binof, nor siphash24 will work with compilers + * lacking compound literal support. Instead, you must use the lower-level + * interfaces which take as parameters the temporary state objects. + * + * o Uppercase macros may evaluate parameters more than once. Lowercase + * macros should not exhibit any such side effects. + * ========================================================================== + */ +#ifndef SIPHASH_H +#define SIPHASH_H + +#include /* size_t */ +#include /* uint64_t uint32_t uint8_t */ +#include "fallthrough.h" + +/* + * Workaround to not require a C++11 compiler for using ULL suffix + * if this code is included and compiled as C++; related GCC warning is: + * warning: use of C++11 long long integer constant [-Wlong-long] + */ +#define SIP_ULL(high, low) ((((uint64_t)high) << 32) | (low)) + +#define SIP_ROTL(x, b) (uint64_t)(((x) << (b)) | ((x) >> (64 - (b)))) + +#define SIP_U32TO8_LE(p, v) \ + (p)[0] = (uint8_t)((v) >> 0); \ + (p)[1] = (uint8_t)((v) >> 8); \ + (p)[2] = (uint8_t)((v) >> 16); \ + (p)[3] = (uint8_t)((v) >> 24); + +#define SIP_U64TO8_LE(p, v) \ + SIP_U32TO8_LE((p) + 0, (uint32_t)((v) >> 0)); \ + SIP_U32TO8_LE((p) + 4, (uint32_t)((v) >> 32)); + +#define SIP_U8TO64_LE(p) \ + (((uint64_t)((p)[0]) << 0) | ((uint64_t)((p)[1]) << 8) \ + | ((uint64_t)((p)[2]) << 16) | ((uint64_t)((p)[3]) << 24) \ + | ((uint64_t)((p)[4]) << 32) | ((uint64_t)((p)[5]) << 40) \ + | ((uint64_t)((p)[6]) << 48) | ((uint64_t)((p)[7]) << 56)) + +#define SIPHASH_INITIALIZER {0, 0, 0, 0, {0}, 0, 0} + +struct siphash { + uint64_t v0, v1, v2, v3; + + unsigned char buf[8], *p; + uint64_t c; +}; /* struct siphash */ + +#define SIP_KEYLEN 16 + +struct sipkey { + uint64_t k[2]; +}; /* struct sipkey */ + +#define sip_keyof(k) sip_tokey(&(struct sipkey){{0}}, (k)) + +static struct sipkey * +sip_tokey(struct sipkey *key, const void *src) { + key->k[0] = SIP_U8TO64_LE((const unsigned char *)src); + key->k[1] = SIP_U8TO64_LE((const unsigned char *)src + 8); + return key; +} /* sip_tokey() */ + +#ifdef SIPHASH_TOBIN + +# define sip_binof(v) sip_tobin((unsigned char[8]){0}, (v)) + +static void * +sip_tobin(void *dst, uint64_t u64) { + SIP_U64TO8_LE((unsigned char *)dst, u64); + return dst; +} /* sip_tobin() */ + +#endif /* SIPHASH_TOBIN */ + +static void +sip_round(struct siphash *H, const int rounds) { + int i; + + for (i = 0; i < rounds; i++) { + H->v0 += H->v1; + H->v1 = SIP_ROTL(H->v1, 13); + H->v1 ^= H->v0; + H->v0 = SIP_ROTL(H->v0, 32); + + H->v2 += H->v3; + H->v3 = SIP_ROTL(H->v3, 16); + H->v3 ^= H->v2; + + H->v0 += H->v3; + H->v3 = SIP_ROTL(H->v3, 21); + H->v3 ^= H->v0; + + H->v2 += H->v1; + H->v1 = SIP_ROTL(H->v1, 17); + H->v1 ^= H->v2; + H->v2 = SIP_ROTL(H->v2, 32); + } +} /* sip_round() */ + +static struct siphash * +sip24_init(struct siphash *H, const struct sipkey *key) { + H->v0 = SIP_ULL(0x736f6d65U, 0x70736575U) ^ key->k[0]; + H->v1 = SIP_ULL(0x646f7261U, 0x6e646f6dU) ^ key->k[1]; + H->v2 = SIP_ULL(0x6c796765U, 0x6e657261U) ^ key->k[0]; + H->v3 = SIP_ULL(0x74656462U, 0x79746573U) ^ key->k[1]; + + H->p = H->buf; + H->c = 0; + + return H; +} /* sip24_init() */ + +#define sip_endof(a) (&(a)[sizeof(a) / sizeof *(a)]) + +static struct siphash * +sip24_update(struct siphash *H, const void *src, size_t len) { + const unsigned char *p = (const unsigned char *)src, *pe = p + len; + uint64_t m; + + do { + while (p < pe && H->p < sip_endof(H->buf)) + *H->p++ = *p++; + + if (H->p < sip_endof(H->buf)) + break; + + m = SIP_U8TO64_LE(H->buf); + H->v3 ^= m; + sip_round(H, 2); + H->v0 ^= m; + + H->p = H->buf; + H->c += 8; + } while (p < pe); + + return H; +} /* sip24_update() */ + +static uint64_t +sip24_final(struct siphash *H) { + const char left = (char)(H->p - H->buf); + uint64_t b = (H->c + left) << 56; + + switch (left) { + case 7: + b |= (uint64_t)H->buf[6] << 48; + EXPAT_FALLTHROUGH; + case 6: + b |= (uint64_t)H->buf[5] << 40; + EXPAT_FALLTHROUGH; + case 5: + b |= (uint64_t)H->buf[4] << 32; + EXPAT_FALLTHROUGH; + case 4: + b |= (uint64_t)H->buf[3] << 24; + EXPAT_FALLTHROUGH; + case 3: + b |= (uint64_t)H->buf[2] << 16; + EXPAT_FALLTHROUGH; + case 2: + b |= (uint64_t)H->buf[1] << 8; + EXPAT_FALLTHROUGH; + case 1: + b |= (uint64_t)H->buf[0] << 0; + EXPAT_FALLTHROUGH; + case 0: + break; + } + + H->v3 ^= b; + sip_round(H, 2); + H->v0 ^= b; + H->v2 ^= 0xff; + sip_round(H, 4); + + return H->v0 ^ H->v1 ^ H->v2 ^ H->v3; +} /* sip24_final() */ + +static uint64_t +siphash24(const void *src, size_t len, const struct sipkey *key) { + struct siphash state = SIPHASH_INITIALIZER; + return sip24_final(sip24_update(sip24_init(&state, key), src, len)); +} /* siphash24() */ + +/* + * SipHash-2-4 output with + * k = 00 01 02 ... + * and + * in = (empty string) + * in = 00 (1 byte) + * in = 00 01 (2 bytes) + * in = 00 01 02 (3 bytes) + * ... + * in = 00 01 02 ... 3e (63 bytes) + */ +static int +sip24_valid(void) { + /* clang-format off */ + static const unsigned char vectors[64][8] = { + { 0x31, 0x0e, 0x0e, 0xdd, 0x47, 0xdb, 0x6f, 0x72, }, + { 0xfd, 0x67, 0xdc, 0x93, 0xc5, 0x39, 0xf8, 0x74, }, + { 0x5a, 0x4f, 0xa9, 0xd9, 0x09, 0x80, 0x6c, 0x0d, }, + { 0x2d, 0x7e, 0xfb, 0xd7, 0x96, 0x66, 0x67, 0x85, }, + { 0xb7, 0x87, 0x71, 0x27, 0xe0, 0x94, 0x27, 0xcf, }, + { 0x8d, 0xa6, 0x99, 0xcd, 0x64, 0x55, 0x76, 0x18, }, + { 0xce, 0xe3, 0xfe, 0x58, 0x6e, 0x46, 0xc9, 0xcb, }, + { 0x37, 0xd1, 0x01, 0x8b, 0xf5, 0x00, 0x02, 0xab, }, + { 0x62, 0x24, 0x93, 0x9a, 0x79, 0xf5, 0xf5, 0x93, }, + { 0xb0, 0xe4, 0xa9, 0x0b, 0xdf, 0x82, 0x00, 0x9e, }, + { 0xf3, 0xb9, 0xdd, 0x94, 0xc5, 0xbb, 0x5d, 0x7a, }, + { 0xa7, 0xad, 0x6b, 0x22, 0x46, 0x2f, 0xb3, 0xf4, }, + { 0xfb, 0xe5, 0x0e, 0x86, 0xbc, 0x8f, 0x1e, 0x75, }, + { 0x90, 0x3d, 0x84, 0xc0, 0x27, 0x56, 0xea, 0x14, }, + { 0xee, 0xf2, 0x7a, 0x8e, 0x90, 0xca, 0x23, 0xf7, }, + { 0xe5, 0x45, 0xbe, 0x49, 0x61, 0xca, 0x29, 0xa1, }, + { 0xdb, 0x9b, 0xc2, 0x57, 0x7f, 0xcc, 0x2a, 0x3f, }, + { 0x94, 0x47, 0xbe, 0x2c, 0xf5, 0xe9, 0x9a, 0x69, }, + { 0x9c, 0xd3, 0x8d, 0x96, 0xf0, 0xb3, 0xc1, 0x4b, }, + { 0xbd, 0x61, 0x79, 0xa7, 0x1d, 0xc9, 0x6d, 0xbb, }, + { 0x98, 0xee, 0xa2, 0x1a, 0xf2, 0x5c, 0xd6, 0xbe, }, + { 0xc7, 0x67, 0x3b, 0x2e, 0xb0, 0xcb, 0xf2, 0xd0, }, + { 0x88, 0x3e, 0xa3, 0xe3, 0x95, 0x67, 0x53, 0x93, }, + { 0xc8, 0xce, 0x5c, 0xcd, 0x8c, 0x03, 0x0c, 0xa8, }, + { 0x94, 0xaf, 0x49, 0xf6, 0xc6, 0x50, 0xad, 0xb8, }, + { 0xea, 0xb8, 0x85, 0x8a, 0xde, 0x92, 0xe1, 0xbc, }, + { 0xf3, 0x15, 0xbb, 0x5b, 0xb8, 0x35, 0xd8, 0x17, }, + { 0xad, 0xcf, 0x6b, 0x07, 0x63, 0x61, 0x2e, 0x2f, }, + { 0xa5, 0xc9, 0x1d, 0xa7, 0xac, 0xaa, 0x4d, 0xde, }, + { 0x71, 0x65, 0x95, 0x87, 0x66, 0x50, 0xa2, 0xa6, }, + { 0x28, 0xef, 0x49, 0x5c, 0x53, 0xa3, 0x87, 0xad, }, + { 0x42, 0xc3, 0x41, 0xd8, 0xfa, 0x92, 0xd8, 0x32, }, + { 0xce, 0x7c, 0xf2, 0x72, 0x2f, 0x51, 0x27, 0x71, }, + { 0xe3, 0x78, 0x59, 0xf9, 0x46, 0x23, 0xf3, 0xa7, }, + { 0x38, 0x12, 0x05, 0xbb, 0x1a, 0xb0, 0xe0, 0x12, }, + { 0xae, 0x97, 0xa1, 0x0f, 0xd4, 0x34, 0xe0, 0x15, }, + { 0xb4, 0xa3, 0x15, 0x08, 0xbe, 0xff, 0x4d, 0x31, }, + { 0x81, 0x39, 0x62, 0x29, 0xf0, 0x90, 0x79, 0x02, }, + { 0x4d, 0x0c, 0xf4, 0x9e, 0xe5, 0xd4, 0xdc, 0xca, }, + { 0x5c, 0x73, 0x33, 0x6a, 0x76, 0xd8, 0xbf, 0x9a, }, + { 0xd0, 0xa7, 0x04, 0x53, 0x6b, 0xa9, 0x3e, 0x0e, }, + { 0x92, 0x59, 0x58, 0xfc, 0xd6, 0x42, 0x0c, 0xad, }, + { 0xa9, 0x15, 0xc2, 0x9b, 0xc8, 0x06, 0x73, 0x18, }, + { 0x95, 0x2b, 0x79, 0xf3, 0xbc, 0x0a, 0xa6, 0xd4, }, + { 0xf2, 0x1d, 0xf2, 0xe4, 0x1d, 0x45, 0x35, 0xf9, }, + { 0x87, 0x57, 0x75, 0x19, 0x04, 0x8f, 0x53, 0xa9, }, + { 0x10, 0xa5, 0x6c, 0xf5, 0xdf, 0xcd, 0x9a, 0xdb, }, + { 0xeb, 0x75, 0x09, 0x5c, 0xcd, 0x98, 0x6c, 0xd0, }, + { 0x51, 0xa9, 0xcb, 0x9e, 0xcb, 0xa3, 0x12, 0xe6, }, + { 0x96, 0xaf, 0xad, 0xfc, 0x2c, 0xe6, 0x66, 0xc7, }, + { 0x72, 0xfe, 0x52, 0x97, 0x5a, 0x43, 0x64, 0xee, }, + { 0x5a, 0x16, 0x45, 0xb2, 0x76, 0xd5, 0x92, 0xa1, }, + { 0xb2, 0x74, 0xcb, 0x8e, 0xbf, 0x87, 0x87, 0x0a, }, + { 0x6f, 0x9b, 0xb4, 0x20, 0x3d, 0xe7, 0xb3, 0x81, }, + { 0xea, 0xec, 0xb2, 0xa3, 0x0b, 0x22, 0xa8, 0x7f, }, + { 0x99, 0x24, 0xa4, 0x3c, 0xc1, 0x31, 0x57, 0x24, }, + { 0xbd, 0x83, 0x8d, 0x3a, 0xaf, 0xbf, 0x8d, 0xb7, }, + { 0x0b, 0x1a, 0x2a, 0x32, 0x65, 0xd5, 0x1a, 0xea, }, + { 0x13, 0x50, 0x79, 0xa3, 0x23, 0x1c, 0xe6, 0x60, }, + { 0x93, 0x2b, 0x28, 0x46, 0xe4, 0xd7, 0x06, 0x66, }, + { 0xe1, 0x91, 0x5f, 0x5c, 0xb1, 0xec, 0xa4, 0x6c, }, + { 0xf3, 0x25, 0x96, 0x5c, 0xa1, 0x6d, 0x62, 0x9f, }, + { 0x57, 0x5f, 0xf2, 0x8e, 0x60, 0x38, 0x1b, 0xe5, }, + { 0x72, 0x45, 0x06, 0xeb, 0x4c, 0x32, 0x8a, 0x95, } + }; + /* clang-format on */ + + unsigned char in[64]; + struct sipkey k; + size_t i; + + sip_tokey(&k, "\000\001\002\003\004\005\006\007\010\011" + "\012\013\014\015\016\017"); + + for (i = 0; i < sizeof in; ++i) { + in[i] = (unsigned char)i; + + if (siphash24(in, i, &k) != SIP_U8TO64_LE(vectors[i])) + return 0; + } + + return 1; +} /* sip24_valid() */ + +#ifdef SIPHASH_MAIN + +# include + +int +main(void) { + const int ok = sip24_valid(); + + if (ok) + puts("OK"); + else + puts("FAIL"); + + return ! ok; +} /* main() */ + +#endif /* SIPHASH_MAIN */ + +#endif /* SIPHASH_H */ diff --git a/port/third_party/expat/utf8tab.h b/port/third_party/expat/utf8tab.h new file mode 100644 index 00000000..73732d15 --- /dev/null +++ b/port/third_party/expat/utf8tab.h @@ -0,0 +1,68 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2017 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +/* 0x80 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0x84 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0x88 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0x8C */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0x90 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0x94 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0x98 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0x9C */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xA0 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xA4 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xA8 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xAC */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xB0 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xB4 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xB8 */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xBC */ BT_TRAIL, BT_TRAIL, BT_TRAIL, BT_TRAIL, + /* 0xC0 */ BT_LEAD2, BT_LEAD2, BT_LEAD2, BT_LEAD2, + /* 0xC4 */ BT_LEAD2, BT_LEAD2, BT_LEAD2, BT_LEAD2, + /* 0xC8 */ BT_LEAD2, BT_LEAD2, BT_LEAD2, BT_LEAD2, + /* 0xCC */ BT_LEAD2, BT_LEAD2, BT_LEAD2, BT_LEAD2, + /* 0xD0 */ BT_LEAD2, BT_LEAD2, BT_LEAD2, BT_LEAD2, + /* 0xD4 */ BT_LEAD2, BT_LEAD2, BT_LEAD2, BT_LEAD2, + /* 0xD8 */ BT_LEAD2, BT_LEAD2, BT_LEAD2, BT_LEAD2, + /* 0xDC */ BT_LEAD2, BT_LEAD2, BT_LEAD2, BT_LEAD2, + /* 0xE0 */ BT_LEAD3, BT_LEAD3, BT_LEAD3, BT_LEAD3, + /* 0xE4 */ BT_LEAD3, BT_LEAD3, BT_LEAD3, BT_LEAD3, + /* 0xE8 */ BT_LEAD3, BT_LEAD3, BT_LEAD3, BT_LEAD3, + /* 0xEC */ BT_LEAD3, BT_LEAD3, BT_LEAD3, BT_LEAD3, + /* 0xF0 */ BT_LEAD4, BT_LEAD4, BT_LEAD4, BT_LEAD4, + /* 0xF4 */ BT_LEAD4, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0xF8 */ BT_NONXML, BT_NONXML, BT_NONXML, BT_NONXML, + /* 0xFC */ BT_NONXML, BT_NONXML, BT_MALFORM, BT_MALFORM, diff --git a/port/third_party/expat/winconfig.h b/port/third_party/expat/winconfig.h new file mode 100644 index 00000000..9e7e8bdc --- /dev/null +++ b/port/third_party/expat/winconfig.h @@ -0,0 +1,50 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Greg Stein + Copyright (c) 2005 Karl Waclawek + Copyright (c) 2017-2023 Sebastian Pipping + Copyright (c) 2023 Orgad Shaneh + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#ifndef WINCONFIG_H +#define WINCONFIG_H + +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +#endif +#include +#undef WIN32_LEAN_AND_MEAN + +#include +#include + +#endif /* ndef WINCONFIG_H */ diff --git a/port/third_party/expat/xcsinc.c b/port/third_party/expat/xcsinc.c new file mode 100644 index 00000000..675b2844 --- /dev/null +++ b/port/third_party/expat/xcsinc.c @@ -0,0 +1,54 @@ +/* This file is included from other .c files! + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 2022-2026 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#if defined(XML_UNICODE) && defined(XML_UNICODE_WCHAR_T) +# include +#endif + +static size_t +xcslen(const XML_Char *s) { +#ifdef XML_UNICODE +# ifdef XML_UNICODE_WCHAR_T + return wcslen(s); +# else + // XML_Char is unsigned short + size_t len = 0; + while (s[len]) { + len++; + } + return len; +# endif +#else + return strlen(s); +#endif +} diff --git a/port/third_party/expat/xmlparse.c b/port/third_party/expat/xmlparse.c new file mode 100644 index 00000000..c3642bd5 --- /dev/null +++ b/port/third_party/expat/xmlparse.c @@ -0,0 +1,9445 @@ +/* 0864fe2d216f47b742263b698bc051c865b342e8b820e42c234717098ea507e3 (2.8.5+) + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2000-2006 Fred L. Drake, Jr. + Copyright (c) 2001-2002 Greg Stein + Copyright (c) 2002-2016 Karl Waclawek + Copyright (c) 2005-2009 Steven Solie + Copyright (c) 2016 Eric Rahm + Copyright (c) 2016-2026 Sebastian Pipping + Copyright (c) 2016 Gaurav + Copyright (c) 2016 Thomas Beutlich + Copyright (c) 2016 Gustavo Grieco + Copyright (c) 2016 Pascal Cuoq + Copyright (c) 2016 Ed Schouten + Copyright (c) 2017-2022 Rhodri James + Copyright (c) 2017 Václav Slavík + Copyright (c) 2017 Viktor Szakats + Copyright (c) 2017 Chanho Park + Copyright (c) 2017 Rolf Eike Beer + Copyright (c) 2017 Hans Wennborg + Copyright (c) 2018 Anton Maklakov + Copyright (c) 2018 Benjamin Peterson + Copyright (c) 2018 Marco Maggi + Copyright (c) 2018 Mariusz Zaborski + Copyright (c) 2019 David Loffredo + Copyright (c) 2019-2020 Ben Wagner + Copyright (c) 2019 Vadim Zeitlin + Copyright (c) 2021 Donghee Na + Copyright (c) 2022 Samanta Navarro + Copyright (c) 2022 Jeffrey Walton + Copyright (c) 2022 Jann Horn + Copyright (c) 2022 Sean McBride + Copyright (c) 2023 Owain Davies + Copyright (c) 2023-2024 Sony Corporation / Snild Dolkow + Copyright (c) 2024-2025 Berkay Eren Ürün + Copyright (c) 2024 Hanno Böck + Copyright (c) 2025-2026 Matthew Fernandez + Copyright (c) 2025 Atrem Borovik + Copyright (c) 2025 Alfonso Gregory + Copyright (c) 2026 Rosen Penev + Copyright (c) 2026 Francesco Bertolaccini + Copyright (c) 2026 Christian Ng + Copyright (c) 2026 Nick Begg + Copyright (c) 2026 Kartik Kenchi + Copyright (c) 2026 Haris Hussain + Copyright (c) 2026 Evgeny Kotkov + Copyright (c) 2026 Darren Carreras + Copyright (c) 2026 Alberto Maschietto + Copyright (c) 2026 Zeyou Liu + Copyright (c) 2026 Stan Ulbrych + Copyright (c) 2026 Braian Plaku + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#define XML_BUILDING_EXPAT 1 + +#include "expat_config.h" + +#if ! defined(XML_GE) || (1 - XML_GE - 1 == 2) || (XML_GE < 0) || (XML_GE > 1) +# error XML_GE (for general entities) must be defined, non-empty, either 1 or 0 (0 to disable, 1 to enable; 1 is a common default) +#endif + +#if defined(XML_DTD) && XML_GE == 0 +# error Either undefine XML_DTD or define XML_GE to 1. +#endif + +#if ! defined(XML_CONTEXT_BYTES) || (1 - XML_CONTEXT_BYTES - 1 == 2) \ + || (XML_CONTEXT_BYTES + 0 < 0) +# error XML_CONTEXT_BYTES must be defined, non-empty and >=0 (0 to disable, >=1 to enable; 1024 is a common default) +#endif + +#include +#include +#include /* memset(), memcpy() */ +#include +#include /* INT_MAX, UINT_MAX */ +#include /* fprintf */ +#include /* getenv */ +#include /* SIZE_MAX, UINT64_MAX, uint64_t, uintptr_t */ +#include /* isnan */ +#include + +#ifdef _WIN32 +# define getpid GetCurrentProcessId +#else +# include /* gettimeofday() */ +# include /* getpid() */ +# include /* getpid() */ +# include /* O_RDONLY */ +# include +#endif + +#ifdef _WIN32 +# include "winconfig.h" +#endif + +#include "ascii.h" +#include "expat.h" +#include "siphash.h" +#include "xcsinc.c" + +#if defined(HAVE_ARC4RANDOM) +# include "random_arc4random.h" +#endif /* defined(HAVE_ARC4RANDOM) */ + +#if defined(HAVE_ARC4RANDOM_BUF) +# include "random_arc4random_buf.h" +#endif // defined(HAVE_ARC4RANDOM_BUF) + +#if defined(XML_DEV_URANDOM) +# include "random_dev_urandom.h" +#endif /* defined(XML_DEV_URANDOM) */ + +#if defined(HAVE_GETENTROPY) +# include "random_getentropy.h" +#endif // defined(HAVE_GETENTROPY) + +#if defined(HAVE_GETRANDOM) || defined(HAVE_SYSCALL_GETRANDOM) +# include "random_getrandom.h" +#endif /* defined(HAVE_GETRANDOM) || defined(HAVE_SYSCALL_GETRANDOM) */ + +#if defined(_WIN32) +# include "random_rand_s.h" +#endif /* defined(_WIN32) */ + +#if ! defined(HAVE_GETRANDOM) && ! defined(HAVE_SYSCALL_GETRANDOM) \ + && ! defined(HAVE_ARC4RANDOM_BUF) && ! defined(HAVE_ARC4RANDOM) \ + && ! defined(HAVE_GETENTROPY) && ! defined(XML_DEV_URANDOM) \ + && ! defined(_WIN32) && ! defined(XML_POOR_ENTROPY) +# error You do not have support for any sources of high quality entropy \ + enabled. For end user security, that is probably not what you want. \ + \ + Your options include: \ + * Linux >=3.17 + glibc >=2.25 (getrandom): HAVE_GETRANDOM, \ + * Linux >=3.17 + glibc (including <2.25) (syscall SYS_getrandom): HAVE_SYSCALL_GETRANDOM, \ + * BSD / macOS >=10.7 / glibc >=2.36 (arc4random_buf): HAVE_ARC4RANDOM_BUF, \ + * BSD / macOS (including <10.7) / glibc >=2.36 (arc4random): HAVE_ARC4RANDOM, \ + * BSD / macOS >=10.12 / glibc >=2.25 (getentropy): HAVE_GETENTROPY, \ + * Linux (including <3.17) / BSD / macOS (including <10.7) / Solaris >=8 (/dev/urandom): XML_DEV_URANDOM, \ + * Windows >=Vista (rand_s): _WIN32. \ + \ + If you insist on not using any of these, bypass this error by defining \ + XML_POOR_ENTROPY and be vulnerable to hash flooding; you have been warned. \ + \ + If you have reasons to patch this detection code away or need changes \ + to the build system, please open a bug. Thank you! +#endif + +#ifdef XML_UNICODE +# define XML_ENCODE_MAX XML_UTF16_ENCODE_MAX +# define XmlConvert XmlUtf16Convert +# define XmlGetInternalEncoding XmlGetUtf16InternalEncoding +# define XmlGetInternalEncodingNS XmlGetUtf16InternalEncodingNS +# define XmlEncode XmlUtf16Encode +# define MUST_CONVERT(enc, s) (! (enc)->isUtf16 || (((uintptr_t)(s)) & 1)) +typedef unsigned short ICHAR; +#else +# define XML_ENCODE_MAX XML_UTF8_ENCODE_MAX +# define XmlConvert XmlUtf8Convert +# define XmlGetInternalEncoding XmlGetUtf8InternalEncoding +# define XmlGetInternalEncodingNS XmlGetUtf8InternalEncodingNS +# define XmlEncode XmlUtf8Encode +# define MUST_CONVERT(enc, s) (! (enc)->isUtf8) +typedef char ICHAR; +#endif + +#ifndef XML_NS + +# define XmlInitEncodingNS XmlInitEncoding +# define XmlInitUnknownEncodingNS XmlInitUnknownEncoding +# undef XmlGetInternalEncodingNS +# define XmlGetInternalEncodingNS XmlGetInternalEncoding +# define XmlParseXmlDeclNS XmlParseXmlDecl + +#endif + +#ifdef XML_UNICODE + +# ifdef XML_UNICODE_WCHAR_T +# define XML_T(x) (const wchar_t) x +# define XML_L(x) L##x +# else +# define XML_T(x) (const unsigned short)x +# define XML_L(x) x +# endif + +#else + +# define XML_T(x) x +# define XML_L(x) x + +#endif + +/* Round up n to be a multiple of sz, where sz is a power of 2. */ +#define ROUND_UP(n, sz) (((n) + ((sz) - 1)) & ~((sz) - 1)) + +/* Do safe (NULL-aware) pointer arithmetic */ +#define EXPAT_SAFE_PTR_DIFF(p, q) (((p) && (q)) ? ((p) - (q)) : 0) + +#define EXPAT_MIN(a, b) (((a) < (b)) ? (a) : (b)) + +#include "internal.h" +#include "hash_table.h" +#include "xmltok.h" +#include "xmlrole.h" + +static void copy_salt_to_sipkey(XML_Parser parser, struct sipkey *key); + +/* For probing (after a collision) we need a step size relative prime + to the hash table size, which is a power of 2. We use double-hashing, + since we can calculate a second hash value cheaply by taking those bits + of the first hash value that were discarded (masked out) when the table + index was calculated: index = hash & mask, where mask = table->size - 1. + We limit the maximum step size to table->size / 4 (mask >> 2) and make + it odd, since odd numbers are always relative prime to a power of 2. +*/ +#define SECOND_HASH(hash, mask, power) \ + ((((hash) & ~(mask)) >> ((power) - 1)) & ((mask) >> 2)) +#define PROBE_STEP(hash, mask, power) \ + ((unsigned char)((SECOND_HASH(hash, mask, power)) | 1)) + +#define INIT_TAG_BUF_SIZE 32 /* must be a multiple of sizeof(XML_Char) */ +#define INIT_DATA_BUF_SIZE 1024 +#define INIT_ATTS_SIZE 16 +#define INIT_ATTS_VERSION 0xFFFFFFFF +#define INIT_BLOCK_SIZE 1024 +#define INIT_BUFFER_SIZE 1024 + +#define EXPAND_SPARE 24 + +typedef struct binding { + struct prefix *prefix; + struct binding *nextTagBinding; + struct binding *prevPrefixBinding; + const struct attribute_id *attId; + XML_Char *uri; + size_t uriLen; + size_t uriAlloc; +} BINDING; + +typedef struct prefix { + const XML_Char *name; + BINDING *binding; +} PREFIX; + +typedef struct { + const XML_Char *str; + const XML_Char *localPart; + const XML_Char *prefix; + size_t strLen; + size_t uriLen; + size_t prefixLen; +} TAG_NAME; + +/* TAG represents an open element. + The name of the element is stored in both the document and API + encodings. The memory buffer 'buf' is a separately-allocated + memory area which stores the name. During the XML_Parse()/ + XML_ParseBuffer() when the element is open, the memory for the 'raw' + version of the name (in the document encoding) is shared with the + document buffer. If the element is open across calls to + XML_Parse()/XML_ParseBuffer(), the buffer is re-allocated to + contain the 'raw' name as well. + + A parser reuses these structures, maintaining a list of allocated + TAG objects in a free list. +*/ +typedef struct tag { + struct tag *parent; /* parent of this element */ + const char *rawName; /* tagName in the original encoding */ + int rawNameLength; + TAG_NAME name; /* tagName in the API encoding */ + union { + char *raw; /* for byte-level access (rawName storage) */ + XML_Char *str; /* for character-level access (converted name) */ + } buf; /* buffer for name components */ + char *bufEnd; /* end of the buffer */ + BINDING *bindings; +} TAG; + +typedef struct { + const XML_Char *name; + const XML_Char *textPtr; + int textLen; /* length in XML_Chars */ + int processed; /* # of processed bytes - when suspended */ + const XML_Char *systemId; + const XML_Char *base; + const XML_Char *publicId; + const XML_Char *notation; + bool open; + bool hasMore; /* true if entity has not been completely processed */ + /* An entity can be open while being already completely processed (!hasMore). + The reason is the delayed closing of entities until their inner + entities are processed and closed */ + bool is_param; + bool is_internal; /* true if declared in internal subset outside PE */ +} ENTITY; + +typedef struct { + enum XML_Content_Type type; + enum XML_Content_Quant quant; + const XML_Char *name; + int firstchild; + int lastchild; + int childcnt; + int nextsib; +} CONTENT_SCAFFOLD; + +#define INIT_SCAFFOLD_ELEMENTS 32 + +typedef struct block { + struct block *next; + int size; + XML_Char s[]; +} BLOCK; + +typedef struct { + BLOCK *blocks; + BLOCK *freeBlocks; + const XML_Char *end; + XML_Char *ptr; + XML_Char *start; + XML_Parser parser; +} STRING_POOL; + +/* The XML_Char before the name is used to determine whether + an attribute has been specified. */ +typedef struct attribute_id { + XML_Char *name; + PREFIX *prefix; + bool maybeTokenized; + bool xmlns; +} ATTRIBUTE_ID; + +typedef struct { + const ATTRIBUTE_ID *id; + XML_Bool isCdata; + const XML_Char *value; +} DEFAULT_ATTRIBUTE; + +// This structure allows mapping attribute names to instances of +// `DEFAULT_ATTRIBUTE`. +typedef struct { + // Member `name` goes first to make this structure compatible with structure + // `NAMED` (further up), which is needed to support use of structure + // `NAME_AND_DEFAULT_ATTRIBUTE` in a hash table as implemented by function + // `lookup` (further down). + const XML_Char *name; + // We would store a `DEFAULT_ATTRIBUTE *` here but the backing array + // can be reallocated which would invalidate the pointer. Using an index + // into the array instead, avoids that problem. + size_t attIndex; + // This is set to `false` by function `lookup`. + bool initialized; +} NAME_AND_DEFAULT_ATTRIBUTE; + +typedef struct { + unsigned long version; + unsigned long hash; + const XML_Char *uriName; +} NS_ATT; + +typedef struct { + const XML_Char *name; + PREFIX *prefix; + const ATTRIBUTE_ID *idAtt; + size_t nDefaultAtts; + size_t allocDefaultAtts; + DEFAULT_ATTRIBUTE *defaultAtts; + HASH_TABLE defaultAttForName; +} ELEMENT_TYPE; + +typedef struct { + HASH_TABLE generalEntities; + HASH_TABLE elementTypes; + HASH_TABLE attributeIds; + HASH_TABLE prefixes; + STRING_POOL pool; + STRING_POOL entityValuePool; + /* false once a parameter entity reference has been skipped */ + XML_Bool keepProcessing; + /* true once an internal or external PE reference has been encountered; + this includes the reference to an external subset */ + XML_Bool hasParamEntityRefs; + XML_Bool standalone; +#ifdef XML_DTD + /* indicates if external PE has been read */ + bool paramEntityRead; + HASH_TABLE paramEntities; +#endif /* XML_DTD */ + PREFIX defaultPrefix; + /* === scaffolding for building content model === */ + bool in_eldecl; + CONTENT_SCAFFOLD *scaffold; + unsigned contentStringLen; + unsigned scaffSize; + unsigned scaffCount; + int scaffLevel; + int *scaffIndex; + size_t scaffIndexSize; +} DTD; + +enum EntityType { + ENTITY_INTERNAL, + ENTITY_ATTRIBUTE, + ENTITY_VALUE, +}; + +typedef struct open_internal_entity { + const char *internalEventPtr; + const char *internalEventEndPtr; + struct open_internal_entity *next; + ENTITY *entity; + int startTagLevel; + bool betweenDecl; /* WFC: PE Between Declarations */ + enum EntityType type; +} OPEN_INTERNAL_ENTITY; + +enum XML_Account { + XML_ACCOUNT_DIRECT, /* bytes directly passed to the Expat parser */ + XML_ACCOUNT_ENTITY_EXPANSION, /* intermediate bytes produced during entity + expansion */ + XML_ACCOUNT_NONE /* i.e. do not account, was accounted already */ +}; + +#if XML_GE == 1 +typedef unsigned long long XmlBigCount; +typedef struct accounting { + XmlBigCount countBytesDirect; + XmlBigCount countBytesIndirect; + unsigned long debugLevel; + float maximumAmplificationFactor; // >=1.0 + unsigned long long activationThresholdBytes; +} ACCOUNTING; + +typedef struct MALLOC_TRACKER { + XmlBigCount bytesAllocated; + XmlBigCount peakBytesAllocated; // updated live only for debug level >=2 + unsigned long debugLevel; + float maximumAmplificationFactor; // >=1.0 + XmlBigCount activationThresholdBytes; +} MALLOC_TRACKER; + +typedef struct entity_stats { + unsigned int countEverOpened; + unsigned int currentDepth; + unsigned int maximumDepthSeen; + unsigned long debugLevel; +} ENTITY_STATS; +#endif /* XML_GE == 1 */ + +typedef enum XML_Error Processor(XML_Parser parser, const char *start, + const char *end, const char **endPtr); + +static Processor prologProcessor; +static Processor prologInitProcessor; +static Processor contentProcessor; +static Processor cdataSectionProcessor; +#ifdef XML_DTD +static Processor ignoreSectionProcessor; +static Processor externalParEntProcessor; +static Processor externalParEntInitProcessor; +static Processor entityValueProcessor; +static Processor entityValueInitProcessor; +#endif /* XML_DTD */ +static Processor epilogProcessor; +static Processor errorProcessor; +static Processor externalEntityInitProcessor; +static Processor externalEntityInitProcessor2; +static Processor externalEntityInitProcessor3; +static Processor externalEntityContentProcessor; +static Processor internalEntityProcessor; + +static enum XML_Error handleUnknownEncoding(XML_Parser parser, + const XML_Char *encodingName); +static enum XML_Error processXmlDecl(XML_Parser parser, int isGeneralTextEntity, + const char *s, const char *next); +static enum XML_Error initializeEncoding(XML_Parser parser); +static enum XML_Error doProlog(XML_Parser parser, const ENCODING *enc, + const char *s, const char *end, int tok, + const char *next, const char **nextPtr, + XML_Bool haveMore, XML_Bool allowClosingDoctype, + enum XML_Account account); +static enum XML_Error processEntity(XML_Parser parser, ENTITY *entity, + bool betweenDecl, enum EntityType type); +static enum XML_Error doContent(XML_Parser parser, int startTagLevel, + const ENCODING *enc, const char *start, + const char *end, const char **endPtr, + XML_Bool haveMore, enum XML_Account account); +static enum XML_Error doCdataSection(XML_Parser parser, const ENCODING *enc, + const char **startPtr, const char *end, + const char **nextPtr, XML_Bool haveMore, + enum XML_Account account); +#ifdef XML_DTD +static enum XML_Error doIgnoreSection(XML_Parser parser, const ENCODING *enc, + const char **startPtr, const char *end, + const char **nextPtr, XML_Bool haveMore); +#endif /* XML_DTD */ + +static void freeBindings(XML_Parser parser, BINDING *bindings); +static enum XML_Error storeAtts(XML_Parser parser, const ENCODING *enc, + const char *attStr, TAG_NAME *tagNamePtr, + BINDING **bindingsPtr, + enum XML_Account account); +static enum XML_Error addBinding(XML_Parser parser, PREFIX *prefix, + const ATTRIBUTE_ID *attId, const XML_Char *uri, + BINDING **bindingsPtr); +static int defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, + XML_Bool isCdata, XML_Bool isId, + const XML_Char *value, XML_Parser parser); +static enum XML_Error storeAttributeValue(XML_Parser parser, + const ENCODING *enc, XML_Bool isCdata, + const char *ptr, const char *end, + STRING_POOL *pool, + enum XML_Account account); +static enum XML_Error +appendAttributeValue(XML_Parser parser, const ENCODING *enc, XML_Bool isCdata, + const char *ptr, const char *end, STRING_POOL *pool, + enum XML_Account account, const char **nextPtr); +static ATTRIBUTE_ID *getAttributeId(XML_Parser parser, const ENCODING *enc, + const char *start, const char *end); +static int setElementTypePrefix(XML_Parser parser, ELEMENT_TYPE *elementType); +#if XML_GE == 1 +static enum XML_Error storeEntityValue(XML_Parser parser, const ENCODING *enc, + const char *start, const char *end, + enum XML_Account account, + const char **nextPtr); +static enum XML_Error callStoreEntityValue(XML_Parser parser, + const ENCODING *enc, + const char *start, const char *end, + enum XML_Account account); +#else +static enum XML_Error storeSelfEntityValue(XML_Parser parser, ENTITY *entity); +#endif +static int reportProcessingInstruction(XML_Parser parser, const ENCODING *enc, + const char *start, const char *end); +static int reportComment(XML_Parser parser, const ENCODING *enc, + const char *start, const char *end); +static void reportDefault(XML_Parser parser, const ENCODING *enc, + const char *start, const char *end); + +static const XML_Char *getContext(XML_Parser parser); +static XML_Bool setContext(XML_Parser parser, const XML_Char *context); + +static void normalizePublicId(XML_Char *s); + +static DTD *dtdCreate(XML_Parser parser); +/* do not call if m_parentParser != NULL */ +static void dtdReset(DTD *p, XML_Parser parser); +static void dtdDestroy(DTD *p, XML_Bool isDocEntity, XML_Parser parser); +static int dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd, + XML_Parser parser); +static int copyEntityTable(XML_Parser oldParser, HASH_TABLE *newTable, + STRING_POOL *newPool, const HASH_TABLE *oldTable); + +static void poolInit(STRING_POOL *pool, XML_Parser parser); +static void poolClear(STRING_POOL *pool); +static void poolDestroy(STRING_POOL *pool); +static XML_Char *poolAppend(STRING_POOL *pool, const ENCODING *enc, + const char *ptr, const char *end); +static XML_Char *poolStoreString(STRING_POOL *pool, const ENCODING *enc, + const char *ptr, const char *end); +static XML_Bool poolGrow(STRING_POOL *pool); +static bool poolGrowUntil(STRING_POOL *pool, size_t needed); +static const XML_Char *poolCopyString(STRING_POOL *pool, const XML_Char *s); +static const XML_Char *poolCopyStringNoFinish(STRING_POOL *pool, + const XML_Char *s); +static const XML_Char *poolCopyStringN(STRING_POOL *pool, const XML_Char *s, + int n); +static const XML_Char *poolAppendString(STRING_POOL *pool, const XML_Char *s); + +static int nextScaffoldPart(XML_Parser parser); +static XML_Content *build_model(XML_Parser parser); +static ELEMENT_TYPE *getElementType(XML_Parser parser, const ENCODING *enc, + const char *ptr, const char *end); + +static XML_Char *copyString(const XML_Char *s, XML_Parser parser); + +static struct sipkey generate_hash_secret_salt(void); +static XML_Bool startParsing(XML_Parser parser); + +static XML_Parser parserCreate(const XML_Char *encodingName, + const XML_Memory_Handling_Suite *memsuite, + const XML_Char *nameSep, DTD *dtd, + XML_Parser parentParser); + +static void parserInit(XML_Parser parser, const XML_Char *encodingName); + +#if XML_GE == 1 +static float accountingGetCurrentAmplification(XML_Parser rootParser); +static void accountingReportStats(XML_Parser originParser, const char *epilog); +static void accountingOnAbort(XML_Parser originParser); +static void accountingReportDiff(XML_Parser rootParser, + unsigned int levelsAwayFromRootParser, + const char *before, const char *after, + ptrdiff_t bytesMore, int source_line, + enum XML_Account account); +static XML_Bool accountingDiffTolerated(XML_Parser originParser, int tok, + const char *before, const char *after, + int source_line, + enum XML_Account account); + +static void entityTrackingReportStats(XML_Parser parser, ENTITY *entity, + const char *action, int sourceLine); +static void entityTrackingOnOpen(XML_Parser parser, ENTITY *entity, + int sourceLine); +static void entityTrackingOnClose(XML_Parser parser, ENTITY *entity, + int sourceLine); +#endif /* XML_GE == 1 */ + +static XML_Parser getRootParserOf(XML_Parser parser, + unsigned int *outLevelDiff); + +static unsigned long getDebugLevel(const char *variableName, + unsigned long defaultDebugLevel); + +static bool poolAppendChar(STRING_POOL *pool, XML_Char c); + +static bool poolAppendChars(STRING_POOL *pool, const XML_Char *s, size_t len); + +#define poolStart(pool) ((pool)->start) +#define poolLength(pool) ((pool)->ptr - (pool)->start) +#define poolChop(pool) ((void)--(pool->ptr)) +#define poolLastChar(pool) (((pool)->ptr)[-1]) +#define poolDiscard(pool) ((pool)->ptr = (pool)->start) +#define poolFinish(pool) ((pool)->start = (pool)->ptr) + +bool +poolAppendChar(STRING_POOL *pool, XML_Char c) { + if (pool->ptr == pool->end && ! poolGrow(pool)) + return false; + + *(pool->ptr)++ = c; + return true; +} + +bool +poolAppendChars(STRING_POOL *pool, const XML_Char *s, size_t len) { + // Detect and prevent integer overflow + if (len > SIZE_MAX / sizeof(XML_Char)) + return false; + + if (! poolGrowUntil(pool, len)) + return false; + + memcpy(pool->ptr, s, len * sizeof(XML_Char)); + pool->ptr += len; + + return true; +} + +#if ! defined(XML_TESTING) +const +#endif + XML_Bool g_reparseDeferralEnabledDefault + = XML_TRUE; // write ONLY in runtests.c +#if defined(XML_TESTING) +unsigned int g_bytesScanned = 0; // used for testing only +#endif + +struct XML_ParserStruct { + /* The first member must be m_userData so that the XML_GetUserData + macro works. */ + void *m_userData; + void *m_handlerArg; + + // How the four parse buffer pointers below relate in time and space: + // + // m_buffer <= m_bufferPtr <= m_bufferEnd <= m_bufferLim + // | | | | + // <--parsed-->| | | + // <---parsing--->| | + // <--unoccupied-->| + // <---------total-malloced/realloced-------->| + + char *m_buffer; // malloc/realloc base pointer of parse buffer + const XML_Memory_Handling_Suite m_mem; + const char *m_bufferPtr; // first character to be parsed + char *m_bufferEnd; // past last character to be parsed + const char *m_bufferLim; // allocated end of m_buffer + + uint64_t m_parseEndByteIndex; + const char *m_parseEndPtr; + size_t m_partialTokenBytesBefore; /* used in heuristic to avoid O(n^2) */ + XML_Bool m_reparseDeferralEnabled; + int m_lastBufferRequestSize; + XML_Char *m_dataBuf; + XML_Char *m_dataBufEnd; + XML_StartElementHandler m_startElementHandler; + XML_EndElementHandler m_endElementHandler; + XML_CharacterDataHandler m_characterDataHandler; + XML_ProcessingInstructionHandler m_processingInstructionHandler; + XML_CommentHandler m_commentHandler; + XML_StartCdataSectionHandler m_startCdataSectionHandler; + XML_EndCdataSectionHandler m_endCdataSectionHandler; + XML_DefaultHandler m_defaultHandler; + XML_StartDoctypeDeclHandler m_startDoctypeDeclHandler; + XML_EndDoctypeDeclHandler m_endDoctypeDeclHandler; + XML_UnparsedEntityDeclHandler m_unparsedEntityDeclHandler; + XML_NotationDeclHandler m_notationDeclHandler; + XML_StartNamespaceDeclHandler m_startNamespaceDeclHandler; + XML_EndNamespaceDeclHandler m_endNamespaceDeclHandler; + XML_NotStandaloneHandler m_notStandaloneHandler; + XML_ExternalEntityRefHandler m_externalEntityRefHandler; + XML_Parser m_externalEntityRefHandlerArg; + XML_SkippedEntityHandler m_skippedEntityHandler; + XML_UnknownEncodingHandler m_unknownEncodingHandler; + XML_ElementDeclHandler m_elementDeclHandler; + XML_AttlistDeclHandler m_attlistDeclHandler; + XML_EntityDeclHandler m_entityDeclHandler; + XML_XmlDeclHandler m_xmlDeclHandler; + const ENCODING *m_encoding; + INIT_ENCODING m_initEncoding; + const ENCODING *m_internalEncoding; + const XML_Char *m_protocolEncodingName; + XML_Bool m_ns; + XML_Bool m_ns_triplets; + void *m_unknownEncodingMem; + void *m_unknownEncodingData; + void *m_unknownEncodingHandlerData; + // Application callback invoked by callUnknownEncodingConvert. + int(XMLCALL *m_unknownEncodingConvert)(void *, const char *); + void(XMLCALL *m_unknownEncodingRelease)(void *); + PROLOG_STATE m_prologState; + Processor *m_processor; + enum XML_Error m_errorCode; + const char *m_eventPtr; + const char *m_eventEndPtr; + const char *m_positionPtr; + OPEN_INTERNAL_ENTITY *m_openInternalEntities; + OPEN_INTERNAL_ENTITY *m_openAttributeEntities; + OPEN_INTERNAL_ENTITY *m_openValueEntities; + OPEN_INTERNAL_ENTITY *m_freeEntities; + XML_Bool m_defaultExpandInternalEntities; + int m_tagLevel; + ENTITY *m_declEntity; + const XML_Char *m_doctypeName; + const XML_Char *m_doctypeSysid; + const XML_Char *m_doctypePubid; + const XML_Char *m_declAttributeType; + const XML_Char *m_declNotationName; + const XML_Char *m_declNotationPublicId; + ELEMENT_TYPE *m_declElementType; + ATTRIBUTE_ID *m_declAttributeId; + XML_Bool m_declAttributeIsCdata; + XML_Bool m_declAttributeIsId; + DTD *m_dtd; + const XML_Char *m_curBase; + TAG *m_tagStack; + TAG *m_freeTagList; + BINDING *m_inheritedBindings; + BINDING *m_freeBindingList; + size_t m_attsSize; + int m_nSpecifiedAtts; + int m_idAttIndex; + ATTRIBUTE *m_atts; + NS_ATT *m_nsAtts; + unsigned long m_nsAttsVersion; + unsigned char m_nsAttsPower; +#ifdef XML_ATTR_INFO + XML_AttrInfo *m_attInfo; +#endif + POSITION m_position; + STRING_POOL m_tempPool; + STRING_POOL m_temp2Pool; + char *m_groupConnector; + size_t m_groupSize; + XML_Char m_namespaceSeparator; + XML_Parser m_parentParser; + XML_ParsingStatus m_parsingStatus; +#ifdef XML_DTD + XML_Bool m_isParamEntity; + XML_Bool m_useForeignDTD; + enum XML_ParamEntityParsing m_paramEntityParsing; +#endif + struct sipkey m_hash_secret_salt_128; + XML_Bool m_hash_secret_salt_set; +#if XML_GE == 1 + ACCOUNTING m_accounting; + MALLOC_TRACKER m_alloc_tracker; + ENTITY_STATS m_entity_stats; +#endif + XML_Bool m_reenter; + unsigned m_handlerCallDepth; +}; + +#if XML_GE == 1 +# define MALLOC(parser, s) (expat_malloc((parser), (s), __LINE__)) +# define REALLOC(parser, p, s) (expat_realloc((parser), (p), (s), __LINE__)) +# define FREE(parser, p) (expat_free((parser), (p), __LINE__)) +#else +# define MALLOC(parser, s) (parser->m_mem.malloc_fcn((s))) +# define REALLOC(parser, p, s) (parser->m_mem.realloc_fcn((p), (s))) +# define FREE(parser, p) (parser->m_mem.free_fcn((p))) +#endif + +#if XML_GE == 1 +static void +expat_heap_stat(XML_Parser rootParser, char operator, XmlBigCount absDiff, + XmlBigCount newTotal, XmlBigCount peakTotal, int sourceLine) { + // NOTE: This can be +infinity or -nan + const float amplification + = (float)newTotal / (float)rootParser->m_accounting.countBytesDirect; + fprintf( + stderr, + "expat: Allocations(%p): Direct " EXPAT_FMT_ULL("10") ", allocated %c" EXPAT_FMT_ULL( + "10") " to " EXPAT_FMT_ULL("10") " (" EXPAT_FMT_ULL("10") " peak), amplification %8.2f (xmlparse.c:%d)\n", + (void *)rootParser, rootParser->m_accounting.countBytesDirect, operator, + absDiff, newTotal, peakTotal, (double)amplification, sourceLine); +} + +static bool +expat_heap_increase_tolerable(XML_Parser rootParser, XmlBigCount increase, + int sourceLine) { + assert(rootParser != NULL); + assert(increase > 0); + + XmlBigCount newTotal = 0; + bool tolerable = true; + + // Detect integer overflow + if ((XmlBigCount)-1 - rootParser->m_alloc_tracker.bytesAllocated < increase) { + tolerable = false; + } else { + newTotal = rootParser->m_alloc_tracker.bytesAllocated + increase; + + if (newTotal >= rootParser->m_alloc_tracker.activationThresholdBytes) { + assert(newTotal > 0); + // NOTE: This can be +infinity when dividing by zero but not -nan + const float amplification + = (float)newTotal / (float)rootParser->m_accounting.countBytesDirect; + if (amplification + > rootParser->m_alloc_tracker.maximumAmplificationFactor) { + tolerable = false; + } + } + } + + if (! tolerable && (rootParser->m_alloc_tracker.debugLevel >= 1)) { + expat_heap_stat(rootParser, '+', increase, newTotal, newTotal, sourceLine); + } + + return tolerable; +} + +# if defined(XML_TESTING) +void * +# else +static void * +# endif +expat_malloc(XML_Parser parser, size_t size, int sourceLine) { + // Detect integer overflow + if (SIZE_MAX - size < sizeof(size_t) + EXPAT_MALLOC_PADDING) { + return NULL; + } + + const XML_Parser rootParser = getRootParserOf(parser, NULL); + assert(rootParser->m_parentParser == NULL); + + const size_t bytesToAllocate = sizeof(size_t) + EXPAT_MALLOC_PADDING + size; + + if ((XmlBigCount)-1 - rootParser->m_alloc_tracker.bytesAllocated + < bytesToAllocate) { + return NULL; // i.e. signal integer overflow as out-of-memory + } + + if (! expat_heap_increase_tolerable(rootParser, bytesToAllocate, + sourceLine)) { + return NULL; // i.e. signal violation as out-of-memory + } + + // Actually allocate + void *const mallocedPtr = parser->m_mem.malloc_fcn(bytesToAllocate); + + if (mallocedPtr == NULL) { + return NULL; + } + + // Update in-block recorded size + *(size_t *)mallocedPtr = size; + + // Update accounting + rootParser->m_alloc_tracker.bytesAllocated += bytesToAllocate; + + // Report as needed + if (rootParser->m_alloc_tracker.debugLevel >= 2) { + if (rootParser->m_alloc_tracker.bytesAllocated + > rootParser->m_alloc_tracker.peakBytesAllocated) { + rootParser->m_alloc_tracker.peakBytesAllocated + = rootParser->m_alloc_tracker.bytesAllocated; + } + expat_heap_stat(rootParser, '+', bytesToAllocate, + rootParser->m_alloc_tracker.bytesAllocated, + rootParser->m_alloc_tracker.peakBytesAllocated, sourceLine); + } + + return (char *)mallocedPtr + sizeof(size_t) + EXPAT_MALLOC_PADDING; +} + +# if defined(XML_TESTING) +void +# else +static void +# endif +expat_free(XML_Parser parser, void *ptr, int sourceLine) { + assert(parser != NULL); + + if (ptr == NULL) { + return; + } + + const XML_Parser rootParser = getRootParserOf(parser, NULL); + assert(rootParser->m_parentParser == NULL); + + // Extract size (to the eyes of malloc_fcn/realloc_fcn) and + // the original pointer returned by malloc/realloc + void *const mallocedPtr = (char *)ptr - EXPAT_MALLOC_PADDING - sizeof(size_t); + const size_t bytesAllocated + = sizeof(size_t) + EXPAT_MALLOC_PADDING + *(size_t *)mallocedPtr; + + // Update accounting + assert(rootParser->m_alloc_tracker.bytesAllocated >= bytesAllocated); + rootParser->m_alloc_tracker.bytesAllocated -= bytesAllocated; + + // Report as needed + if (rootParser->m_alloc_tracker.debugLevel >= 2) { + expat_heap_stat(rootParser, '-', bytesAllocated, + rootParser->m_alloc_tracker.bytesAllocated, + rootParser->m_alloc_tracker.peakBytesAllocated, sourceLine); + } + + // NOTE: This may be freeing rootParser, so freeing has to come last + parser->m_mem.free_fcn(mallocedPtr); +} + +# if defined(XML_TESTING) +void * +# else +static void * +# endif +expat_realloc(XML_Parser parser, void *ptr, size_t size, int sourceLine) { + assert(parser != NULL); + + if (ptr == NULL) { + return expat_malloc(parser, size, sourceLine); + } + + if (size == 0) { + expat_free(parser, ptr, sourceLine); + return NULL; + } + + const XML_Parser rootParser = getRootParserOf(parser, NULL); + assert(rootParser->m_parentParser == NULL); + + // Extract original size (to the eyes of the caller) and the original + // pointer returned by malloc/realloc + void *mallocedPtr = (char *)ptr - EXPAT_MALLOC_PADDING - sizeof(size_t); + const size_t prevSize = *(size_t *)mallocedPtr; + + // Classify upcoming change + const bool isIncrease = (size > prevSize); + const size_t absDiff + = (size > prevSize) ? (size - prevSize) : (prevSize - size); + + // Ask for permission from accounting + if (isIncrease) { + if (! expat_heap_increase_tolerable(rootParser, absDiff, sourceLine)) { + return NULL; // i.e. signal violation as out-of-memory + } + } + + // NOTE: Integer overflow detection has already been done for us + // by expat_heap_increase_tolerable(..) above + assert(SIZE_MAX - sizeof(size_t) - EXPAT_MALLOC_PADDING >= size); + + // Actually allocate + mallocedPtr = parser->m_mem.realloc_fcn( + mallocedPtr, sizeof(size_t) + EXPAT_MALLOC_PADDING + size); + + if (mallocedPtr == NULL) { + return NULL; + } + + // Update accounting + if (isIncrease) { + assert((XmlBigCount)-1 - rootParser->m_alloc_tracker.bytesAllocated + >= absDiff); + rootParser->m_alloc_tracker.bytesAllocated += absDiff; + } else { // i.e. decrease + assert(rootParser->m_alloc_tracker.bytesAllocated >= absDiff); + rootParser->m_alloc_tracker.bytesAllocated -= absDiff; + } + + // Report as needed + if (rootParser->m_alloc_tracker.debugLevel >= 2) { + if (rootParser->m_alloc_tracker.bytesAllocated + > rootParser->m_alloc_tracker.peakBytesAllocated) { + rootParser->m_alloc_tracker.peakBytesAllocated + = rootParser->m_alloc_tracker.bytesAllocated; + } + expat_heap_stat(rootParser, isIncrease ? '+' : '-', absDiff, + rootParser->m_alloc_tracker.bytesAllocated, + rootParser->m_alloc_tracker.peakBytesAllocated, sourceLine); + } + + // Update in-block recorded size + *(size_t *)mallocedPtr = size; + + return (char *)mallocedPtr + sizeof(size_t) + EXPAT_MALLOC_PADDING; +} +#endif // XML_GE == 1 + +XML_Parser XMLCALL +XML_ParserCreate(const XML_Char *encodingName) { + return XML_ParserCreate_MM(encodingName, NULL, NULL); +} + +XML_Parser XMLCALL +XML_ParserCreateNS(const XML_Char *encodingName, XML_Char nsSep) { + XML_Char tmp[2] = {nsSep, 0}; + return XML_ParserCreate_MM(encodingName, NULL, tmp); +} + +// "xml=http://www.w3.org/XML/1998/namespace" +static const XML_Char implicitContext[] + = {ASCII_x, ASCII_m, ASCII_l, ASCII_EQUALS, ASCII_h, + ASCII_t, ASCII_t, ASCII_p, ASCII_COLON, ASCII_SLASH, + ASCII_SLASH, ASCII_w, ASCII_w, ASCII_w, ASCII_PERIOD, + ASCII_w, ASCII_3, ASCII_PERIOD, ASCII_o, ASCII_r, + ASCII_g, ASCII_SLASH, ASCII_X, ASCII_M, ASCII_L, + ASCII_SLASH, ASCII_1, ASCII_9, ASCII_9, ASCII_8, + ASCII_SLASH, ASCII_n, ASCII_a, ASCII_m, ASCII_e, + ASCII_s, ASCII_p, ASCII_a, ASCII_c, ASCII_e, + '\0'}; + +#if ! defined(HAVE_ARC4RANDOM_BUF) && ! defined(HAVE_ARC4RANDOM) + +static unsigned long +gather_time_entropy(void) { +# ifdef _WIN32 + FILETIME ft; + GetSystemTimeAsFileTime(&ft); /* never fails */ + return ft.dwHighDateTime ^ ft.dwLowDateTime; +# else + struct timeval tv; + int gettimeofday_res; + + gettimeofday_res = gettimeofday(&tv, NULL); + +# if defined(NDEBUG) + (void)gettimeofday_res; +# else + assert(gettimeofday_res == 0); +# endif /* defined(NDEBUG) */ + + /* Microseconds time is <20 bits entropy */ + return tv.tv_usec; +# endif +} + +#endif /* ! defined(HAVE_ARC4RANDOM_BUF) && ! defined(HAVE_ARC4RANDOM) */ + +static struct sipkey +ENTROPY_DEBUG(const char *label, struct sipkey entropy_128) { + if (getDebugLevel("EXPAT_ENTROPY_DEBUG", 0) >= 1u) { + fprintf(stderr, + "expat: Entropy: %s --> [0x" EXPAT_FMT_LLX( + "016") ", 0x" EXPAT_FMT_LLX("016") "] (16 bytes)\n", + label, (unsigned long long)entropy_128.k[0], + (unsigned long long)entropy_128.k[1]); + } + return entropy_128; +} + +static struct sipkey +generate_hash_secret_salt(void) { + struct sipkey entropy; + + /* "Failproof" high quality providers: */ +#if defined(HAVE_ARC4RANDOM_BUF) + writeRandomBytes_arc4random_buf(&entropy, sizeof(entropy)); + return ENTROPY_DEBUG("arc4random_buf", entropy); +#elif defined(HAVE_ARC4RANDOM) + writeRandomBytes_arc4random(&entropy, sizeof(entropy)); + return ENTROPY_DEBUG("arc4random", entropy); +#else + /* Try high quality providers first .. */ +# ifdef _WIN32 + if (writeRandomBytes_rand_s(&entropy, sizeof(entropy))) { + return ENTROPY_DEBUG("rand_s", entropy); + } +# elif defined(HAVE_GETENTROPY) + if (writeRandomBytes_getentropy(&entropy, sizeof(entropy))) { + return ENTROPY_DEBUG("getentropy", entropy); + } + errno = 0; +# elif defined(HAVE_GETRANDOM) || defined(HAVE_SYSCALL_GETRANDOM) + if (writeRandomBytes_getrandom_nonblock(&entropy, sizeof(entropy))) { + return ENTROPY_DEBUG("getrandom", entropy); + } +# endif +# if ! defined(_WIN32) && defined(XML_DEV_URANDOM) + if (writeRandomBytes_dev_urandom(&entropy, sizeof(entropy))) { + return ENTROPY_DEBUG("/dev/urandom", entropy); + } +# endif /* ! defined(_WIN32) && defined(XML_DEV_URANDOM) */ + /* .. and self-made low quality for backup: */ + + entropy.k[0] = 0; + entropy.k[1] = gather_time_entropy(); +# if ! defined(__wasi__) + /* Process ID is 0 bits entropy if attacker has local access */ + entropy.k[1] ^= getpid(); +# endif + + /* Factors are 2^31-1 and 2^61-1 (Mersenne primes M31 and M61) */ + if (sizeof(unsigned long) == 4) { + entropy.k[1] *= 2147483647; + return ENTROPY_DEBUG("fallback(4)", entropy); + } else { + entropy.k[1] *= 2305843009213693951ULL; + return ENTROPY_DEBUG("fallback(8)", entropy); + } +#endif +} + +static void +beforeHandler(XML_Parser parser) { + assert(parser->m_handlerCallDepth < UINT_MAX); + parser->m_handlerCallDepth++; +} + +static void +afterHandler(XML_Parser parser) { + assert(parser->m_handlerCallDepth > 0); + parser->m_handlerCallDepth--; +} + +static bool +isCalledFromInsideHandler(XML_Parser parser) { + return parser->m_handlerCallDepth > 0; +} + +static void +callUnknownEncodingRelease(XML_Parser parser) { + beforeHandler(parser); + parser->m_unknownEncodingRelease(parser->m_unknownEncodingData); + afterHandler(parser); + parser->m_unknownEncodingRelease = NULL; + parser->m_unknownEncodingData = NULL; +} + +static int XMLCALL +callUnknownEncodingConvert(void *data, const char *p) { + XML_Parser parser = data; + beforeHandler(parser); + const int result + = parser->m_unknownEncodingConvert(parser->m_unknownEncodingData, p); + afterHandler(parser); + return result; +} + +static enum XML_Error +callProcessor(XML_Parser parser, const char *start, const char *end, + const char **endPtr) { + const size_t have_now = EXPAT_SAFE_PTR_DIFF(end, start); + + if (parser->m_reparseDeferralEnabled + && ! parser->m_parsingStatus.finalBuffer) { + // Heuristic: don't try to parse a partial token again until the amount of + // available data has increased significantly. + const size_t had_before = parser->m_partialTokenBytesBefore; + // ...but *do* try anyway if we're close to causing a reallocation. + size_t available_buffer + = EXPAT_SAFE_PTR_DIFF(parser->m_bufferPtr, parser->m_buffer); +#if XML_CONTEXT_BYTES > 0 + available_buffer -= EXPAT_MIN(available_buffer, XML_CONTEXT_BYTES); +#endif + available_buffer + += EXPAT_SAFE_PTR_DIFF(parser->m_bufferLim, parser->m_bufferEnd); + // m_lastBufferRequestSize is never assigned a value < 0, so the cast is ok + const bool enough + = (have_now >= 2 * had_before) + || ((size_t)parser->m_lastBufferRequestSize > available_buffer); + + if (! enough) { + *endPtr = start; // callers may expect this to be set + return XML_ERROR_NONE; + } + } +#if defined(XML_TESTING) + g_bytesScanned += (unsigned)have_now; +#endif + // Run in a loop to eliminate dangerous recursion depths + enum XML_Error ret; + *endPtr = start; + while (1) { + // Use endPtr as the new start in each iteration, since it will + // be set to the next start point by m_processor. + ret = parser->m_processor(parser, *endPtr, end, endPtr); + + // Make parsing status (and in particular XML_SUSPENDED) take + // precedence over re-enter flag when they disagree + if (parser->m_parsingStatus.parsing != XML_PARSING) { + parser->m_reenter = XML_FALSE; + } + + if (! parser->m_reenter) { + break; + } + + parser->m_reenter = XML_FALSE; + if (ret != XML_ERROR_NONE) + return ret; + } + + if (ret == XML_ERROR_NONE) { + // if we consumed nothing, remember what we had on this parse attempt. + if (*endPtr == start) { + parser->m_partialTokenBytesBefore = have_now; + } else { + parser->m_partialTokenBytesBefore = 0; + } + } + return ret; +} + +static XML_Bool /* only valid for root parser */ +startParsing(XML_Parser parser) { + /* hash functions must be initialized before setContext() is called */ + if (parser->m_hash_secret_salt_set != XML_TRUE) { + parser->m_hash_secret_salt_128 = generate_hash_secret_salt(); + parser->m_hash_secret_salt_set = XML_TRUE; + } + if (parser->m_ns) { + /* implicit context only set for root parser, since child + parsers (i.e. external entity parsers) will inherit it + */ + return setContext(parser, implicitContext); + } + return XML_TRUE; +} + +XML_Parser XMLCALL +XML_ParserCreate_MM(const XML_Char *encodingName, + const XML_Memory_Handling_Suite *memsuite, + const XML_Char *nameSep) { + return parserCreate(encodingName, memsuite, nameSep, NULL, NULL); +} + +static XML_Parser +parserCreate(const XML_Char *encodingName, + const XML_Memory_Handling_Suite *memsuite, const XML_Char *nameSep, + DTD *dtd, XML_Parser parentParser) { + XML_Parser parser = NULL; + +#if XML_GE == 1 + const size_t increase + = sizeof(size_t) + EXPAT_MALLOC_PADDING + sizeof(struct XML_ParserStruct); + + if (parentParser != NULL) { + const XML_Parser rootParser = getRootParserOf(parentParser, NULL); + if (! expat_heap_increase_tolerable(rootParser, increase, __LINE__)) { + return NULL; + } + } +#else + UNUSED_P(parentParser); +#endif + + if (memsuite) { + XML_Memory_Handling_Suite *mtemp; +#if XML_GE == 1 + void *const sizeAndParser + = memsuite->malloc_fcn(sizeof(size_t) + EXPAT_MALLOC_PADDING + + sizeof(struct XML_ParserStruct)); + if (sizeAndParser != NULL) { + *(size_t *)sizeAndParser = sizeof(struct XML_ParserStruct); + parser = (XML_Parser)((char *)sizeAndParser + sizeof(size_t) + + EXPAT_MALLOC_PADDING); +#else + parser = memsuite->malloc_fcn(sizeof(struct XML_ParserStruct)); + if (parser != NULL) { +#endif + mtemp = (XML_Memory_Handling_Suite *)&(parser->m_mem); + mtemp->malloc_fcn = memsuite->malloc_fcn; + mtemp->realloc_fcn = memsuite->realloc_fcn; + mtemp->free_fcn = memsuite->free_fcn; + } + } else { + XML_Memory_Handling_Suite *mtemp; +#if XML_GE == 1 + void *const sizeAndParser = malloc(sizeof(size_t) + EXPAT_MALLOC_PADDING + + sizeof(struct XML_ParserStruct)); + if (sizeAndParser != NULL) { + *(size_t *)sizeAndParser = sizeof(struct XML_ParserStruct); + parser = (XML_Parser)((char *)sizeAndParser + sizeof(size_t) + + EXPAT_MALLOC_PADDING); +#else + parser = malloc(sizeof(struct XML_ParserStruct)); + if (parser != NULL) { +#endif + mtemp = (XML_Memory_Handling_Suite *)&(parser->m_mem); + mtemp->malloc_fcn = malloc; + mtemp->realloc_fcn = realloc; + mtemp->free_fcn = free; + } + } // cppcheck-suppress[memleak symbolName=sizeAndParser] // Cppcheck >=2.18.0 + + if (! parser) + return parser; + +#if XML_GE == 1 + // Initialize .m_alloc_tracker + memset(&parser->m_alloc_tracker, 0, sizeof(MALLOC_TRACKER)); + if (parentParser == NULL) { + parser->m_alloc_tracker.debugLevel + = getDebugLevel("EXPAT_MALLOC_DEBUG", 0u); + parser->m_alloc_tracker.maximumAmplificationFactor + = EXPAT_ALLOC_TRACKER_MAXIMUM_AMPLIFICATION_DEFAULT; + parser->m_alloc_tracker.activationThresholdBytes + = EXPAT_ALLOC_TRACKER_ACTIVATION_THRESHOLD_DEFAULT; + + // NOTE: This initialization needs to come this early because these fields + // are read by allocation tracking code + parser->m_parentParser = NULL; + parser->m_accounting.countBytesDirect = 0; + } else { + parser->m_parentParser = parentParser; + } + + // Record XML_ParserStruct allocation we did a few lines up before + const XML_Parser rootParser = getRootParserOf(parser, NULL); + assert(rootParser->m_parentParser == NULL); + assert(SIZE_MAX - rootParser->m_alloc_tracker.bytesAllocated >= increase); + rootParser->m_alloc_tracker.bytesAllocated += increase; + + // Report on allocation + if (rootParser->m_alloc_tracker.debugLevel >= 2) { + if (rootParser->m_alloc_tracker.bytesAllocated + > rootParser->m_alloc_tracker.peakBytesAllocated) { + rootParser->m_alloc_tracker.peakBytesAllocated + = rootParser->m_alloc_tracker.bytesAllocated; + } + + expat_heap_stat(rootParser, '+', increase, + rootParser->m_alloc_tracker.bytesAllocated, + rootParser->m_alloc_tracker.peakBytesAllocated, __LINE__); + } +#else + parser->m_parentParser = NULL; +#endif // XML_GE == 1 + + parser->m_buffer = NULL; + parser->m_bufferLim = NULL; + + parser->m_attsSize = INIT_ATTS_SIZE; + parser->m_atts = MALLOC(parser, parser->m_attsSize * sizeof(ATTRIBUTE)); + if (parser->m_atts == NULL) { + FREE(parser, parser); + return NULL; + } +#ifdef XML_ATTR_INFO + parser->m_attInfo = MALLOC(parser, parser->m_attsSize * sizeof(XML_AttrInfo)); + if (parser->m_attInfo == NULL) { + FREE(parser, parser->m_atts); + FREE(parser, parser); + return NULL; + } +#endif + parser->m_dataBuf = MALLOC(parser, INIT_DATA_BUF_SIZE * sizeof(XML_Char)); + if (parser->m_dataBuf == NULL) { + FREE(parser, parser->m_atts); +#ifdef XML_ATTR_INFO + FREE(parser, parser->m_attInfo); +#endif + FREE(parser, parser); + return NULL; + } + parser->m_dataBufEnd = parser->m_dataBuf + INIT_DATA_BUF_SIZE; + + if (dtd) + parser->m_dtd = dtd; + else { + parser->m_dtd = dtdCreate(parser); + if (parser->m_dtd == NULL) { + FREE(parser, parser->m_dataBuf); + FREE(parser, parser->m_atts); +#ifdef XML_ATTR_INFO + FREE(parser, parser->m_attInfo); +#endif + FREE(parser, parser); + return NULL; + } + } + + parser->m_freeBindingList = NULL; + parser->m_freeTagList = NULL; + parser->m_freeEntities = NULL; + + parser->m_groupSize = 0; + parser->m_groupConnector = NULL; + + parser->m_unknownEncodingHandler = NULL; + parser->m_unknownEncodingHandlerData = NULL; + + parser->m_namespaceSeparator = ASCII_EXCL; + parser->m_ns = XML_FALSE; + parser->m_ns_triplets = XML_FALSE; + + parser->m_nsAtts = NULL; + parser->m_nsAttsVersion = 0; + parser->m_nsAttsPower = 0; + + parser->m_protocolEncodingName = NULL; + + poolInit(&parser->m_tempPool, parser); + poolInit(&parser->m_temp2Pool, parser); + parserInit(parser, encodingName); + + if (encodingName && ! parser->m_protocolEncodingName) { + if (dtd) { + // We need to stop the upcoming call to XML_ParserFree from happily + // destroying parser->m_dtd because the DTD is shared with the parent + // parser and the only guard that keeps XML_ParserFree from destroying + // parser->m_dtd is parser->m_isParamEntity but it will be set to + // XML_TRUE only later in XML_ExternalEntityParserCreate (or not at all). + parser->m_dtd = NULL; + } + XML_ParserFree(parser); + return NULL; + } + + if (nameSep) { + parser->m_ns = XML_TRUE; + parser->m_internalEncoding = XmlGetInternalEncodingNS(); + parser->m_namespaceSeparator = *nameSep; + } else { + parser->m_internalEncoding = XmlGetInternalEncoding(); + } + + return parser; +} + +static void +parserInit(XML_Parser parser, const XML_Char *encodingName) { + parser->m_processor = prologInitProcessor; + XmlPrologStateInit(&parser->m_prologState); + if (encodingName != NULL) { + parser->m_protocolEncodingName = copyString(encodingName, parser); + } + parser->m_curBase = NULL; + XmlInitEncoding(&parser->m_initEncoding, &parser->m_encoding, 0); + parser->m_userData = NULL; + parser->m_handlerArg = NULL; + parser->m_startElementHandler = NULL; + parser->m_endElementHandler = NULL; + parser->m_characterDataHandler = NULL; + parser->m_processingInstructionHandler = NULL; + parser->m_commentHandler = NULL; + parser->m_startCdataSectionHandler = NULL; + parser->m_endCdataSectionHandler = NULL; + parser->m_defaultHandler = NULL; + parser->m_startDoctypeDeclHandler = NULL; + parser->m_endDoctypeDeclHandler = NULL; + parser->m_unparsedEntityDeclHandler = NULL; + parser->m_notationDeclHandler = NULL; + parser->m_startNamespaceDeclHandler = NULL; + parser->m_endNamespaceDeclHandler = NULL; + parser->m_notStandaloneHandler = NULL; + parser->m_externalEntityRefHandler = NULL; + parser->m_externalEntityRefHandlerArg = parser; + parser->m_skippedEntityHandler = NULL; + parser->m_elementDeclHandler = NULL; + parser->m_attlistDeclHandler = NULL; + parser->m_entityDeclHandler = NULL; + parser->m_xmlDeclHandler = NULL; + parser->m_bufferPtr = parser->m_buffer; + parser->m_bufferEnd = parser->m_buffer; + parser->m_parseEndByteIndex = 0; + parser->m_parseEndPtr = NULL; + parser->m_partialTokenBytesBefore = 0; + parser->m_reparseDeferralEnabled = g_reparseDeferralEnabledDefault; + parser->m_lastBufferRequestSize = 0; + parser->m_declElementType = NULL; + parser->m_declAttributeId = NULL; + parser->m_declEntity = NULL; + parser->m_doctypeName = NULL; + parser->m_doctypeSysid = NULL; + parser->m_doctypePubid = NULL; + parser->m_declAttributeType = NULL; + parser->m_declNotationName = NULL; + parser->m_declNotationPublicId = NULL; + parser->m_declAttributeIsCdata = XML_FALSE; + parser->m_declAttributeIsId = XML_FALSE; + memset(&parser->m_position, 0, sizeof(POSITION)); + parser->m_errorCode = XML_ERROR_NONE; + parser->m_eventPtr = NULL; + parser->m_eventEndPtr = NULL; + parser->m_positionPtr = NULL; + parser->m_openInternalEntities = NULL; + parser->m_openAttributeEntities = NULL; + parser->m_openValueEntities = NULL; + parser->m_defaultExpandInternalEntities = XML_TRUE; + parser->m_tagLevel = 0; + parser->m_tagStack = NULL; + parser->m_inheritedBindings = NULL; + parser->m_nSpecifiedAtts = 0; + parser->m_unknownEncodingMem = NULL; + parser->m_unknownEncodingConvert = NULL; + parser->m_unknownEncodingRelease = NULL; + parser->m_unknownEncodingData = NULL; + parser->m_parsingStatus.parsing = XML_INITIALIZED; + // Reentry can only be triggered inside m_processor calls + parser->m_reenter = XML_FALSE; + parser->m_handlerCallDepth = 0; +#ifdef XML_DTD + parser->m_isParamEntity = XML_FALSE; + parser->m_useForeignDTD = XML_FALSE; + parser->m_paramEntityParsing = XML_PARAM_ENTITY_PARSING_NEVER; +#endif + parser->m_hash_secret_salt_128.k[0] = 0; + parser->m_hash_secret_salt_128.k[1] = 0; + parser->m_hash_secret_salt_set = XML_FALSE; + +#if XML_GE == 1 + memset(&parser->m_accounting, 0, sizeof(ACCOUNTING)); + parser->m_accounting.debugLevel = getDebugLevel("EXPAT_ACCOUNTING_DEBUG", 0u); + parser->m_accounting.maximumAmplificationFactor + = EXPAT_BILLION_LAUGHS_ATTACK_PROTECTION_MAXIMUM_AMPLIFICATION_DEFAULT; + parser->m_accounting.activationThresholdBytes + = EXPAT_BILLION_LAUGHS_ATTACK_PROTECTION_ACTIVATION_THRESHOLD_DEFAULT; + + memset(&parser->m_entity_stats, 0, sizeof(ENTITY_STATS)); + parser->m_entity_stats.debugLevel = getDebugLevel("EXPAT_ENTITY_DEBUG", 0u); +#endif +} + +/* moves list of bindings to m_freeBindingList */ +static void +moveToFreeBindingList(XML_Parser parser, BINDING *bindings) { + while (bindings) { + BINDING *b = bindings; + bindings = bindings->nextTagBinding; + b->nextTagBinding = parser->m_freeBindingList; + parser->m_freeBindingList = b; + } +} + +/* Moves a list of entities onto the start of another list. */ +static void +moveEntityList(OPEN_INTERNAL_ENTITY **dst, OPEN_INTERNAL_ENTITY **src) { + for (OPEN_INTERNAL_ENTITY *head = *src; head != NULL;) { + OPEN_INTERNAL_ENTITY *const openEntity = head; + head = head->next; + openEntity->next = *dst; + *dst = openEntity; + } +} + +XML_Bool XMLCALL +XML_ParserReset(XML_Parser parser, const XML_Char *encodingName) { + TAG *tStk; + + if ((parser == NULL) || isCalledFromInsideHandler(parser)) + return XML_FALSE; + + if (parser->m_parentParser) + return XML_FALSE; + /* move m_tagStack to m_freeTagList */ + tStk = parser->m_tagStack; + while (tStk) { + TAG *tag = tStk; + tStk = tStk->parent; + tag->parent = parser->m_freeTagList; + moveToFreeBindingList(parser, tag->bindings); + tag->bindings = NULL; + parser->m_freeTagList = tag; + } + /* move m_openInternalEntities to m_freeEntities */ + moveEntityList(&parser->m_freeEntities, &parser->m_openInternalEntities); + /* move m_openAttributeEntities to m_freeEntities (i.e. same task but for + * attributes) */ + moveEntityList(&parser->m_freeEntities, &parser->m_openAttributeEntities); + /* move m_openValueEntities to m_freeEntities (i.e. same task but for value + * entities) */ + moveEntityList(&parser->m_freeEntities, &parser->m_openValueEntities); + moveToFreeBindingList(parser, parser->m_inheritedBindings); + FREE(parser, parser->m_unknownEncodingMem); + if (parser->m_unknownEncodingRelease) + callUnknownEncodingRelease(parser); + poolClear(&parser->m_tempPool); + poolClear(&parser->m_temp2Pool); + FREE(parser, (void *)parser->m_protocolEncodingName); + parser->m_protocolEncodingName = NULL; + parserInit(parser, encodingName); + dtdReset(parser->m_dtd, parser); + return XML_TRUE; +} + +static XML_Bool +parserBusy(XML_Parser parser) { + switch (parser->m_parsingStatus.parsing) { + case XML_PARSING: + case XML_SUSPENDED: + return XML_TRUE; + case XML_INITIALIZED: + case XML_FINISHED: + default: + return XML_FALSE; + } +} + +enum XML_Status XMLCALL +XML_SetEncoding(XML_Parser parser, const XML_Char *encodingName) { + if (parser == NULL) + return XML_STATUS_ERROR; + /* Block after XML_Parse()/XML_ParseBuffer() has been called. + XXX There's no way for the caller to determine which of the + XXX possible error cases caused the XML_STATUS_ERROR return. + */ + if (parserBusy(parser)) + return XML_STATUS_ERROR; + + /* Get rid of any previous encoding name */ + FREE(parser, (void *)parser->m_protocolEncodingName); + + if (encodingName == NULL) + /* No new encoding name */ + parser->m_protocolEncodingName = NULL; + else { + /* Copy the new encoding name into allocated memory */ + parser->m_protocolEncodingName = copyString(encodingName, parser); + if (! parser->m_protocolEncodingName) + return XML_STATUS_ERROR; + } + return XML_STATUS_OK; +} + +XML_Parser XMLCALL +XML_ExternalEntityParserCreate(XML_Parser oldParser, const XML_Char *context, + const XML_Char *encodingName) { + XML_Parser parser = oldParser; + DTD *newDtd = NULL; + DTD *oldDtd; + XML_StartElementHandler oldStartElementHandler; + XML_EndElementHandler oldEndElementHandler; + XML_CharacterDataHandler oldCharacterDataHandler; + XML_ProcessingInstructionHandler oldProcessingInstructionHandler; + XML_CommentHandler oldCommentHandler; + XML_StartCdataSectionHandler oldStartCdataSectionHandler; + XML_EndCdataSectionHandler oldEndCdataSectionHandler; + XML_DefaultHandler oldDefaultHandler; + XML_UnparsedEntityDeclHandler oldUnparsedEntityDeclHandler; + XML_NotationDeclHandler oldNotationDeclHandler; + XML_StartNamespaceDeclHandler oldStartNamespaceDeclHandler; + XML_EndNamespaceDeclHandler oldEndNamespaceDeclHandler; + XML_NotStandaloneHandler oldNotStandaloneHandler; + XML_ExternalEntityRefHandler oldExternalEntityRefHandler; + XML_SkippedEntityHandler oldSkippedEntityHandler; + XML_UnknownEncodingHandler oldUnknownEncodingHandler; + void *oldUnknownEncodingHandlerData; + XML_ElementDeclHandler oldElementDeclHandler; + XML_AttlistDeclHandler oldAttlistDeclHandler; + XML_EntityDeclHandler oldEntityDeclHandler; + XML_XmlDeclHandler oldXmlDeclHandler; + ELEMENT_TYPE *oldDeclElementType; + + void *oldUserData; + void *oldHandlerArg; + XML_Bool oldDefaultExpandInternalEntities; + XML_Parser oldExternalEntityRefHandlerArg; +#ifdef XML_DTD + enum XML_ParamEntityParsing oldParamEntityParsing; + int oldInEntityValue; +#endif + XML_Bool oldns_triplets; + /* Note that the new parser shares the same hash secret as the old + parser, so that dtdCopy and copyEntityTable can lookup values + from hash tables associated with either parser without us having + to worry which hash secrets each table has. + */ + struct sipkey oldhash_secret_salt_128; + XML_Bool oldhash_secret_salt_set; + XML_Bool oldReparseDeferralEnabled; + + /* Validate the oldParser parameter before we pull everything out of it */ + if (oldParser == NULL) + return NULL; + + /* Stash the original parser contents on the stack */ + oldDtd = parser->m_dtd; + oldStartElementHandler = parser->m_startElementHandler; + oldEndElementHandler = parser->m_endElementHandler; + oldCharacterDataHandler = parser->m_characterDataHandler; + oldProcessingInstructionHandler = parser->m_processingInstructionHandler; + oldCommentHandler = parser->m_commentHandler; + oldStartCdataSectionHandler = parser->m_startCdataSectionHandler; + oldEndCdataSectionHandler = parser->m_endCdataSectionHandler; + oldDefaultHandler = parser->m_defaultHandler; + oldUnparsedEntityDeclHandler = parser->m_unparsedEntityDeclHandler; + oldNotationDeclHandler = parser->m_notationDeclHandler; + oldStartNamespaceDeclHandler = parser->m_startNamespaceDeclHandler; + oldEndNamespaceDeclHandler = parser->m_endNamespaceDeclHandler; + oldNotStandaloneHandler = parser->m_notStandaloneHandler; + oldExternalEntityRefHandler = parser->m_externalEntityRefHandler; + oldSkippedEntityHandler = parser->m_skippedEntityHandler; + oldUnknownEncodingHandler = parser->m_unknownEncodingHandler; + oldUnknownEncodingHandlerData = parser->m_unknownEncodingHandlerData; + oldElementDeclHandler = parser->m_elementDeclHandler; + oldAttlistDeclHandler = parser->m_attlistDeclHandler; + oldEntityDeclHandler = parser->m_entityDeclHandler; + oldXmlDeclHandler = parser->m_xmlDeclHandler; + oldDeclElementType = parser->m_declElementType; + + oldUserData = parser->m_userData; + oldHandlerArg = parser->m_handlerArg; + oldDefaultExpandInternalEntities = parser->m_defaultExpandInternalEntities; + oldExternalEntityRefHandlerArg = parser->m_externalEntityRefHandlerArg; +#ifdef XML_DTD + oldParamEntityParsing = parser->m_paramEntityParsing; + oldInEntityValue = parser->m_prologState.inEntityValue; +#endif + oldns_triplets = parser->m_ns_triplets; + /* Note that the new parser shares the same hash secret as the old + parser, so that dtdCopy and copyEntityTable can lookup values + from hash tables associated with either parser without us having + to worry which hash secrets each table has. + */ + oldhash_secret_salt_128 = parser->m_hash_secret_salt_128; + oldhash_secret_salt_set = parser->m_hash_secret_salt_set; + oldReparseDeferralEnabled = parser->m_reparseDeferralEnabled; + +#ifdef XML_DTD + if (! context) + newDtd = oldDtd; +#endif /* XML_DTD */ + + if (parser->m_ns) { + XML_Char tmp[2] = {parser->m_namespaceSeparator, 0}; + parser = parserCreate(encodingName, &parser->m_mem, tmp, newDtd, oldParser); + } else { + parser + = parserCreate(encodingName, &parser->m_mem, NULL, newDtd, oldParser); + } + + if (! parser) + return NULL; + + parser->m_startElementHandler = oldStartElementHandler; + parser->m_endElementHandler = oldEndElementHandler; + parser->m_characterDataHandler = oldCharacterDataHandler; + parser->m_processingInstructionHandler = oldProcessingInstructionHandler; + parser->m_commentHandler = oldCommentHandler; + parser->m_startCdataSectionHandler = oldStartCdataSectionHandler; + parser->m_endCdataSectionHandler = oldEndCdataSectionHandler; + parser->m_defaultHandler = oldDefaultHandler; + parser->m_unparsedEntityDeclHandler = oldUnparsedEntityDeclHandler; + parser->m_notationDeclHandler = oldNotationDeclHandler; + parser->m_startNamespaceDeclHandler = oldStartNamespaceDeclHandler; + parser->m_endNamespaceDeclHandler = oldEndNamespaceDeclHandler; + parser->m_notStandaloneHandler = oldNotStandaloneHandler; + parser->m_externalEntityRefHandler = oldExternalEntityRefHandler; + parser->m_skippedEntityHandler = oldSkippedEntityHandler; + parser->m_unknownEncodingHandler = oldUnknownEncodingHandler; + parser->m_unknownEncodingHandlerData = oldUnknownEncodingHandlerData; + parser->m_elementDeclHandler = oldElementDeclHandler; + parser->m_attlistDeclHandler = oldAttlistDeclHandler; + parser->m_entityDeclHandler = oldEntityDeclHandler; + parser->m_xmlDeclHandler = oldXmlDeclHandler; + parser->m_declElementType = oldDeclElementType; + parser->m_userData = oldUserData; + if (oldUserData == oldHandlerArg) + parser->m_handlerArg = parser->m_userData; + else + parser->m_handlerArg = parser; + if (oldExternalEntityRefHandlerArg != oldParser) + parser->m_externalEntityRefHandlerArg = oldExternalEntityRefHandlerArg; + parser->m_defaultExpandInternalEntities = oldDefaultExpandInternalEntities; + parser->m_ns_triplets = oldns_triplets; + parser->m_hash_secret_salt_128 = oldhash_secret_salt_128; + parser->m_hash_secret_salt_set = oldhash_secret_salt_set; + parser->m_reparseDeferralEnabled = oldReparseDeferralEnabled; + parser->m_parentParser = oldParser; +#ifdef XML_DTD + parser->m_paramEntityParsing = oldParamEntityParsing; + parser->m_prologState.inEntityValue = oldInEntityValue; + if (context) { +#endif /* XML_DTD */ + if (! dtdCopy(oldParser, parser->m_dtd, oldDtd, parser) + || ! setContext(parser, context)) { + XML_ParserFree(parser); + return NULL; + } + parser->m_processor = externalEntityInitProcessor; +#ifdef XML_DTD + } else { + /* The DTD instance referenced by parser->m_dtd is shared between the + document's root parser and external PE parsers, therefore one does not + need to call setContext. In addition, one also *must* not call + setContext, because this would overwrite existing prefix->binding + pointers in parser->m_dtd with ones that get destroyed with the external + PE parser. This would leave those prefixes with dangling pointers. + */ + parser->m_isParamEntity = XML_TRUE; + XmlPrologStateInitExternalEntity(&parser->m_prologState); + parser->m_processor = externalParEntInitProcessor; + } +#endif /* XML_DTD */ + return parser; +} + +static void +destroyBindings(BINDING *bindings, XML_Parser parser) { + for (;;) { + BINDING *b = bindings; + if (! b) + break; + bindings = b->nextTagBinding; + FREE(parser, b->uri); + FREE(parser, b); + } +} + +void XMLCALL +XML_ParserFree(XML_Parser parser) { + TAG *tagList; + if ((parser == NULL) || isCalledFromInsideHandler(parser)) + return; + /* free m_tagStack and m_freeTagList */ + tagList = parser->m_tagStack; + for (;;) { + TAG *p; + if (tagList == NULL) { + if (parser->m_freeTagList == NULL) + break; + tagList = parser->m_freeTagList; + parser->m_freeTagList = NULL; + } + p = tagList; + tagList = tagList->parent; + FREE(parser, p->buf.raw); + destroyBindings(p->bindings, parser); + FREE(parser, p); + } + /* free m_openInternalEntities */ + for (OPEN_INTERNAL_ENTITY *entityList = parser->m_openInternalEntities; + entityList != NULL;) { + OPEN_INTERNAL_ENTITY *const openEntity = entityList; + entityList = entityList->next; + FREE(parser, openEntity); + } + /* free m_openAttributeEntities */ + for (OPEN_INTERNAL_ENTITY *entityList = parser->m_openAttributeEntities; + entityList != NULL;) { + OPEN_INTERNAL_ENTITY *const openEntity = entityList; + entityList = entityList->next; + FREE(parser, openEntity); + } + /* free m_openValueEntities */ + for (OPEN_INTERNAL_ENTITY *entityList = parser->m_openValueEntities; + entityList != NULL;) { + OPEN_INTERNAL_ENTITY *const openEntity = entityList; + entityList = entityList->next; + FREE(parser, openEntity); + } + /* free m_freeEntities */ + for (OPEN_INTERNAL_ENTITY *entityList = parser->m_freeEntities; + entityList != NULL;) { + OPEN_INTERNAL_ENTITY *const openEntity = entityList; + entityList = entityList->next; + FREE(parser, openEntity); + } + parser->m_freeEntities = NULL; + destroyBindings(parser->m_freeBindingList, parser); + destroyBindings(parser->m_inheritedBindings, parser); + poolDestroy(&parser->m_tempPool); + poolDestroy(&parser->m_temp2Pool); + FREE(parser, (void *)parser->m_protocolEncodingName); +#ifdef XML_DTD + /* external parameter entity parsers share the DTD structure + parser->m_dtd with the root parser, so we must not destroy it + */ + if (! parser->m_isParamEntity && parser->m_dtd) +#else + if (parser->m_dtd) +#endif /* XML_DTD */ + dtdDestroy(parser->m_dtd, (XML_Bool)! parser->m_parentParser, parser); + FREE(parser, parser->m_atts); +#ifdef XML_ATTR_INFO + FREE(parser, parser->m_attInfo); +#endif + FREE(parser, parser->m_groupConnector); + // NOTE: We are avoiding FREE(..) here because parser->m_buffer + // is not being allocated with MALLOC(..) but with plain + // .malloc_fcn(..). + parser->m_mem.free_fcn(parser->m_buffer); + FREE(parser, parser->m_dataBuf); + FREE(parser, parser->m_nsAtts); + FREE(parser, parser->m_unknownEncodingMem); + if (parser->m_unknownEncodingRelease) + callUnknownEncodingRelease(parser); + FREE(parser, parser); +} + +void XMLCALL +XML_UseParserAsHandlerArg(XML_Parser parser) { + if (parser != NULL) + parser->m_handlerArg = parser; +} + +enum XML_Error XMLCALL +XML_UseForeignDTD(XML_Parser parser, XML_Bool useDTD) { + if (parser == NULL) + return XML_ERROR_INVALID_ARGUMENT; +#ifdef XML_DTD + /* block after XML_Parse()/XML_ParseBuffer() has been called */ + if (parserBusy(parser)) + return XML_ERROR_CANT_CHANGE_FEATURE_ONCE_PARSING; + parser->m_useForeignDTD = useDTD; + return XML_ERROR_NONE; +#else + UNUSED_P(useDTD); + return XML_ERROR_FEATURE_REQUIRES_XML_DTD; +#endif +} + +void XMLCALL +XML_SetReturnNSTriplet(XML_Parser parser, int do_nst) { + if (parser == NULL) + return; + /* block after XML_Parse()/XML_ParseBuffer() has been called */ + if (parserBusy(parser)) + return; + parser->m_ns_triplets = do_nst ? XML_TRUE : XML_FALSE; +} + +void XMLCALL +XML_SetUserData(XML_Parser parser, void *p) { + if (parser == NULL) + return; + if (parser->m_handlerArg == parser->m_userData) + parser->m_handlerArg = parser->m_userData = p; + else + parser->m_userData = p; +} + +enum XML_Status XMLCALL +XML_SetBase(XML_Parser parser, const XML_Char *p) { + if (parser == NULL) + return XML_STATUS_ERROR; + if (p) { + p = poolCopyString(&parser->m_dtd->pool, p); + if (! p) + return XML_STATUS_ERROR; + parser->m_curBase = p; + } else + parser->m_curBase = NULL; + return XML_STATUS_OK; +} + +const XML_Char *XMLCALL +XML_GetBase(XML_Parser parser) { + if (parser == NULL) + return NULL; + return parser->m_curBase; +} + +int XMLCALL +XML_GetSpecifiedAttributeCount(XML_Parser parser) { + if (parser == NULL) + return -1; + return parser->m_nSpecifiedAtts; +} + +int XMLCALL +XML_GetIdAttributeIndex(XML_Parser parser) { + if (parser == NULL) + return -1; + return parser->m_idAttIndex; +} + +#ifdef XML_ATTR_INFO +const XML_AttrInfo *XMLCALL +XML_GetAttributeInfo(XML_Parser parser) { + if (parser == NULL) + return NULL; + return parser->m_attInfo; +} +#endif + +void XMLCALL +XML_SetElementHandler(XML_Parser parser, XML_StartElementHandler start, + XML_EndElementHandler end) { + if (parser == NULL) + return; + parser->m_startElementHandler = start; + parser->m_endElementHandler = end; +} + +void XMLCALL +XML_SetStartElementHandler(XML_Parser parser, XML_StartElementHandler start) { + if (parser != NULL) + parser->m_startElementHandler = start; +} + +void XMLCALL +XML_SetEndElementHandler(XML_Parser parser, XML_EndElementHandler end) { + if (parser != NULL) + parser->m_endElementHandler = end; +} + +void XMLCALL +XML_SetCharacterDataHandler(XML_Parser parser, + XML_CharacterDataHandler handler) { + if (parser != NULL) + parser->m_characterDataHandler = handler; +} + +void XMLCALL +XML_SetProcessingInstructionHandler(XML_Parser parser, + XML_ProcessingInstructionHandler handler) { + if (parser != NULL) + parser->m_processingInstructionHandler = handler; +} + +void XMLCALL +XML_SetCommentHandler(XML_Parser parser, XML_CommentHandler handler) { + if (parser != NULL) + parser->m_commentHandler = handler; +} + +void XMLCALL +XML_SetCdataSectionHandler(XML_Parser parser, + XML_StartCdataSectionHandler start, + XML_EndCdataSectionHandler end) { + if (parser == NULL) + return; + parser->m_startCdataSectionHandler = start; + parser->m_endCdataSectionHandler = end; +} + +void XMLCALL +XML_SetStartCdataSectionHandler(XML_Parser parser, + XML_StartCdataSectionHandler start) { + if (parser != NULL) + parser->m_startCdataSectionHandler = start; +} + +void XMLCALL +XML_SetEndCdataSectionHandler(XML_Parser parser, + XML_EndCdataSectionHandler end) { + if (parser != NULL) + parser->m_endCdataSectionHandler = end; +} + +void XMLCALL +XML_SetDefaultHandler(XML_Parser parser, XML_DefaultHandler handler) { + if (parser == NULL) + return; + parser->m_defaultHandler = handler; + parser->m_defaultExpandInternalEntities = XML_FALSE; +} + +void XMLCALL +XML_SetDefaultHandlerExpand(XML_Parser parser, XML_DefaultHandler handler) { + if (parser == NULL) + return; + parser->m_defaultHandler = handler; + parser->m_defaultExpandInternalEntities = XML_TRUE; +} + +void XMLCALL +XML_SetDoctypeDeclHandler(XML_Parser parser, XML_StartDoctypeDeclHandler start, + XML_EndDoctypeDeclHandler end) { + if (parser == NULL) + return; + parser->m_startDoctypeDeclHandler = start; + parser->m_endDoctypeDeclHandler = end; +} + +void XMLCALL +XML_SetStartDoctypeDeclHandler(XML_Parser parser, + XML_StartDoctypeDeclHandler start) { + if (parser != NULL) + parser->m_startDoctypeDeclHandler = start; +} + +void XMLCALL +XML_SetEndDoctypeDeclHandler(XML_Parser parser, XML_EndDoctypeDeclHandler end) { + if (parser != NULL) + parser->m_endDoctypeDeclHandler = end; +} + +void XMLCALL +XML_SetUnparsedEntityDeclHandler(XML_Parser parser, + XML_UnparsedEntityDeclHandler handler) { + if (parser != NULL) + parser->m_unparsedEntityDeclHandler = handler; +} + +void XMLCALL +XML_SetNotationDeclHandler(XML_Parser parser, XML_NotationDeclHandler handler) { + if (parser != NULL) + parser->m_notationDeclHandler = handler; +} + +void XMLCALL +XML_SetNamespaceDeclHandler(XML_Parser parser, + XML_StartNamespaceDeclHandler start, + XML_EndNamespaceDeclHandler end) { + if (parser == NULL) + return; + parser->m_startNamespaceDeclHandler = start; + parser->m_endNamespaceDeclHandler = end; +} + +void XMLCALL +XML_SetStartNamespaceDeclHandler(XML_Parser parser, + XML_StartNamespaceDeclHandler start) { + if (parser != NULL) + parser->m_startNamespaceDeclHandler = start; +} + +void XMLCALL +XML_SetEndNamespaceDeclHandler(XML_Parser parser, + XML_EndNamespaceDeclHandler end) { + if (parser != NULL) + parser->m_endNamespaceDeclHandler = end; +} + +void XMLCALL +XML_SetNotStandaloneHandler(XML_Parser parser, + XML_NotStandaloneHandler handler) { + if (parser != NULL) + parser->m_notStandaloneHandler = handler; +} + +void XMLCALL +XML_SetExternalEntityRefHandler(XML_Parser parser, + XML_ExternalEntityRefHandler handler) { + if (parser != NULL) + parser->m_externalEntityRefHandler = handler; +} + +void XMLCALL +XML_SetExternalEntityRefHandlerArg(XML_Parser parser, void *arg) { + if (parser == NULL) + return; + if (arg) + parser->m_externalEntityRefHandlerArg = (XML_Parser)arg; + else + parser->m_externalEntityRefHandlerArg = parser; +} + +void XMLCALL +XML_SetSkippedEntityHandler(XML_Parser parser, + XML_SkippedEntityHandler handler) { + if (parser != NULL) + parser->m_skippedEntityHandler = handler; +} + +void XMLCALL +XML_SetUnknownEncodingHandler(XML_Parser parser, + XML_UnknownEncodingHandler handler, void *data) { + if (parser == NULL) + return; + parser->m_unknownEncodingHandler = handler; + parser->m_unknownEncodingHandlerData = data; +} + +void XMLCALL +XML_SetElementDeclHandler(XML_Parser parser, XML_ElementDeclHandler eldecl) { + if (parser != NULL) + parser->m_elementDeclHandler = eldecl; +} + +void XMLCALL +XML_SetAttlistDeclHandler(XML_Parser parser, XML_AttlistDeclHandler attdecl) { + if (parser != NULL) + parser->m_attlistDeclHandler = attdecl; +} + +void XMLCALL +XML_SetEntityDeclHandler(XML_Parser parser, XML_EntityDeclHandler handler) { + if (parser != NULL) + parser->m_entityDeclHandler = handler; +} + +void XMLCALL +XML_SetXmlDeclHandler(XML_Parser parser, XML_XmlDeclHandler handler) { + if (parser != NULL) + parser->m_xmlDeclHandler = handler; +} + +int XMLCALL +XML_SetParamEntityParsing(XML_Parser parser, + enum XML_ParamEntityParsing peParsing) { + if (parser == NULL) + return 0; + /* block after XML_Parse()/XML_ParseBuffer() has been called */ + if (parserBusy(parser)) + return 0; +#ifdef XML_DTD + parser->m_paramEntityParsing = peParsing; + return 1; +#else + return peParsing == XML_PARAM_ENTITY_PARSING_NEVER; +#endif +} + +/* The body of XML_SetHashSalt, so that Expat's own tests can reach it + without tripping the deprecation of the public function. */ +XML_NONTESTING_STATIC int +xmlSetHashSalt(XML_Parser parser, unsigned long hash_salt) { + if (parser == NULL) + return 0; + + const XML_Parser rootParser = getRootParserOf(parser, NULL); + assert(! rootParser->m_parentParser); + + /* block after XML_Parse()/XML_ParseBuffer() has been called */ + if (parserBusy(rootParser)) + return 0; + + rootParser->m_hash_secret_salt_128.k[0] = 0; + rootParser->m_hash_secret_salt_128.k[1] = hash_salt; + + if (hash_salt != 0) { // to remain backwards compatible + rootParser->m_hash_secret_salt_set = XML_TRUE; + + if (sizeof(unsigned long) == 4) + ENTROPY_DEBUG("explicit(4)", rootParser->m_hash_secret_salt_128); + else + ENTROPY_DEBUG("explicit(8)", rootParser->m_hash_secret_salt_128); + } + + return 1; +} + +// DEPRECATED since Expat 2.8.0. +int XMLCALL +XML_SetHashSalt(XML_Parser parser, unsigned long hash_salt) { + return xmlSetHashSalt(parser, hash_salt); +} + +XML_Bool XMLCALL +XML_SetHashSalt16Bytes(XML_Parser parser, const uint8_t entropy[16]) { + if (parser == NULL) + return XML_FALSE; + + if (entropy == NULL) + return XML_FALSE; + + const XML_Parser rootParser = getRootParserOf(parser, NULL); + assert(! rootParser->m_parentParser); + + /* block after XML_Parse()/XML_ParseBuffer() has been called */ + if (parserBusy(rootParser)) + return XML_FALSE; + + sip_tokey(&(rootParser->m_hash_secret_salt_128), entropy); + + rootParser->m_hash_secret_salt_set = XML_TRUE; + + ENTROPY_DEBUG("explicit(16)", rootParser->m_hash_secret_salt_128); + + return XML_TRUE; +} + +enum XML_Status XMLCALL +XML_Parse(XML_Parser parser, const char *s, int len, int isFinal) { + if ((parser == NULL) || (len < 0) || ((s == NULL) && (len != 0))) { + if (parser != NULL) + parser->m_errorCode = XML_ERROR_INVALID_ARGUMENT; + return XML_STATUS_ERROR; + } + if (isCalledFromInsideHandler(parser)) + return XML_STATUS_ERROR; + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + parser->m_errorCode = XML_ERROR_SUSPENDED; + return XML_STATUS_ERROR; + case XML_FINISHED: + parser->m_errorCode = XML_ERROR_FINISHED; + return XML_STATUS_ERROR; + case XML_INITIALIZED: + if (parser->m_parentParser == NULL && ! startParsing(parser)) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + return XML_STATUS_ERROR; + } + EXPAT_FALLTHROUGH; + default: + parser->m_parsingStatus.parsing = XML_PARSING; + } + +#if XML_CONTEXT_BYTES == 0 + if (parser->m_bufferPtr == parser->m_bufferEnd) { + const char *end; + int nLeftOver; + enum XML_Status result; + /* Detect overflow (a+b > MAX <==> b > MAX-a) */ + if ((uint64_t)len > UINT64_MAX - parser->m_parseEndByteIndex) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + parser->m_eventPtr = parser->m_eventEndPtr = NULL; + parser->m_processor = errorProcessor; + return XML_STATUS_ERROR; + } + // though this isn't a buffer request, we assume that `len` is the app's + // preferred buffer fill size, and therefore save it here. + parser->m_lastBufferRequestSize = len; + parser->m_parseEndByteIndex += len; + parser->m_positionPtr = s; + parser->m_parsingStatus.finalBuffer = (XML_Bool)isFinal; + + parser->m_errorCode + = callProcessor(parser, s, parser->m_parseEndPtr = s + len, &end); + + if (parser->m_errorCode != XML_ERROR_NONE) { + parser->m_eventEndPtr = parser->m_eventPtr; + parser->m_processor = errorProcessor; + return XML_STATUS_ERROR; + } else { + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + result = XML_STATUS_SUSPENDED; + break; + case XML_INITIALIZED: + case XML_PARSING: + if (isFinal) { + parser->m_parsingStatus.parsing = XML_FINISHED; + return XML_STATUS_OK; + } + EXPAT_FALLTHROUGH; + default: + result = XML_STATUS_OK; + } + } + + XmlUpdatePosition(parser->m_encoding, parser->m_positionPtr, end, + &parser->m_position); + nLeftOver = s + len - end; + if (nLeftOver) { + // Back up and restore the parsing status to avoid XML_ERROR_SUSPENDED + // (and XML_ERROR_FINISHED) from XML_GetBuffer. + const enum XML_Parsing originalStatus = parser->m_parsingStatus.parsing; + parser->m_parsingStatus.parsing = XML_PARSING; + void *const temp = XML_GetBuffer(parser, nLeftOver); + parser->m_parsingStatus.parsing = originalStatus; + // GetBuffer may have overwritten this, but we want to remember what the + // app requested, not how many bytes were left over after parsing. + parser->m_lastBufferRequestSize = len; + if (temp == NULL) { + // NOTE: parser->m_errorCode has already been set by XML_GetBuffer(). + parser->m_eventPtr = parser->m_eventEndPtr = NULL; + parser->m_processor = errorProcessor; + return XML_STATUS_ERROR; + } + // Since we know that the buffer was empty and XML_CONTEXT_BYTES is 0, we + // don't have any data to preserve, and can copy straight into the start + // of the buffer rather than the GetBuffer return pointer (which may be + // pointing further into the allocated buffer). + memcpy(parser->m_buffer, end, nLeftOver); + } + parser->m_bufferPtr = parser->m_buffer; + parser->m_bufferEnd = parser->m_buffer + nLeftOver; + parser->m_positionPtr = parser->m_bufferPtr; + parser->m_parseEndPtr = parser->m_bufferEnd; + parser->m_eventPtr = parser->m_bufferPtr; + parser->m_eventEndPtr = parser->m_bufferPtr; + return result; + } +#endif /* XML_CONTEXT_BYTES == 0 */ + void *buff = XML_GetBuffer(parser, len); + if (buff == NULL) + return XML_STATUS_ERROR; + if (len > 0) { + assert(s != NULL); // make sure s==NULL && len!=0 was rejected above + memcpy(buff, s, len); + } + return XML_ParseBuffer(parser, len, isFinal); +} + +enum XML_Status XMLCALL +XML_ParseBuffer(XML_Parser parser, int len, int isFinal) { + const char *start; + enum XML_Status result = XML_STATUS_OK; + + if ((parser == NULL) || isCalledFromInsideHandler(parser)) + return XML_STATUS_ERROR; + + if (len < 0) { + parser->m_errorCode = XML_ERROR_INVALID_ARGUMENT; + return XML_STATUS_ERROR; + } + + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + parser->m_errorCode = XML_ERROR_SUSPENDED; + return XML_STATUS_ERROR; + case XML_FINISHED: + parser->m_errorCode = XML_ERROR_FINISHED; + return XML_STATUS_ERROR; + case XML_INITIALIZED: + /* Has someone called XML_GetBuffer successfully before? */ + if (! parser->m_bufferPtr) { + parser->m_errorCode = XML_ERROR_NO_BUFFER; + return XML_STATUS_ERROR; + } + + if (parser->m_parentParser == NULL && ! startParsing(parser)) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + return XML_STATUS_ERROR; + } + EXPAT_FALLTHROUGH; + default: + parser->m_parsingStatus.parsing = XML_PARSING; + } + + // Detect and avoid integer overflow + if ((uint64_t)len > UINT64_MAX - parser->m_parseEndByteIndex) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + parser->m_eventPtr = parser->m_eventEndPtr = NULL; + parser->m_processor = errorProcessor; + return XML_STATUS_ERROR; + } + + start = parser->m_bufferPtr; + parser->m_positionPtr = start; + parser->m_bufferEnd += len; + parser->m_parseEndPtr = parser->m_bufferEnd; + parser->m_parseEndByteIndex += len; + parser->m_parsingStatus.finalBuffer = (XML_Bool)isFinal; + + parser->m_errorCode = callProcessor(parser, start, parser->m_parseEndPtr, + &parser->m_bufferPtr); + + if (parser->m_errorCode != XML_ERROR_NONE) { + parser->m_eventEndPtr = parser->m_eventPtr; + parser->m_processor = errorProcessor; + return XML_STATUS_ERROR; + } else { + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + result = XML_STATUS_SUSPENDED; + break; + case XML_INITIALIZED: + case XML_PARSING: + if (isFinal) { + parser->m_parsingStatus.parsing = XML_FINISHED; + return result; + } + break; + default:; /* should not happen */ + } + } + + XmlUpdatePosition(parser->m_encoding, parser->m_positionPtr, + parser->m_bufferPtr, &parser->m_position); + parser->m_positionPtr = parser->m_bufferPtr; + return result; +} + +/* Modifies `parser`’s buffer to be backed by `newBuf`. */ +static void +setParserBuffer(XML_Parser parser, char *newBuf, int newBufSize, int keep) { + parser->m_bufferLim = newBuf + newBufSize; + if (parser->m_bufferPtr) { + const int parsing + = (int)EXPAT_SAFE_PTR_DIFF(parser->m_bufferEnd, parser->m_bufferPtr); + memcpy(newBuf, parser->m_bufferPtr - keep, parsing + keep); + // NOTE: We are avoiding FREE(..) here because parser->m_buffer + // is not being allocated with MALLOC(..) but with plain + // .malloc_fcn(..). + parser->m_mem.free_fcn(parser->m_buffer); + parser->m_buffer = newBuf; + parser->m_bufferEnd = newBuf + parsing + keep; + parser->m_bufferPtr = newBuf + keep; + } else { + /* This must be a brand new buffer with no data in it yet */ + parser->m_buffer = newBuf; + parser->m_bufferEnd = newBuf; + parser->m_bufferPtr = newBuf; + } +} + +void *XMLCALL +XML_GetBuffer(XML_Parser parser, int len) { + if ((parser == NULL) || isCalledFromInsideHandler(parser)) + return NULL; + if (len < 0) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + return NULL; + } + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + parser->m_errorCode = XML_ERROR_SUSPENDED; + return NULL; + case XML_FINISHED: + parser->m_errorCode = XML_ERROR_FINISHED; + return NULL; + default:; + } + + // whether or not the request succeeds, `len` seems to be the app's preferred + // buffer fill size; remember it. + parser->m_lastBufferRequestSize = len; + if (len > EXPAT_SAFE_PTR_DIFF(parser->m_bufferLim, parser->m_bufferEnd) + || parser->m_buffer == NULL) { + /* Do not invoke signed arithmetic overflow: */ + int neededSize = (int)((unsigned)len + + (unsigned)EXPAT_SAFE_PTR_DIFF( + parser->m_bufferEnd, parser->m_bufferPtr)); + if (neededSize < 0) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + return NULL; + } +#if XML_CONTEXT_BYTES > 0 + const int parsed + = (int)EXPAT_SAFE_PTR_DIFF(parser->m_bufferPtr, parser->m_buffer); + int keep = parsed; + if (keep > XML_CONTEXT_BYTES) + keep = XML_CONTEXT_BYTES; + /* Detect and prevent integer overflow */ + if (keep > INT_MAX - neededSize) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + return NULL; + } +#else + int keep = 0; +#endif /* XML_CONTEXT_BYTES > 0 */ + neededSize += keep; + if (parser->m_buffer && parser->m_bufferPtr + && neededSize + <= EXPAT_SAFE_PTR_DIFF(parser->m_bufferLim, parser->m_buffer)) { +#if XML_CONTEXT_BYTES > 0 + if (keep < parsed) { + int offset = parsed - keep; + /* The buffer pointers cannot be NULL here; we have at least some bytes + * in the buffer */ + memmove(parser->m_buffer, &parser->m_buffer[offset], + parser->m_bufferEnd - parser->m_bufferPtr + keep); + parser->m_bufferEnd -= offset; + parser->m_bufferPtr -= offset; + } +#else + memmove(parser->m_buffer, parser->m_bufferPtr, + EXPAT_SAFE_PTR_DIFF(parser->m_bufferEnd, parser->m_bufferPtr)); + parser->m_bufferEnd + = parser->m_buffer + + EXPAT_SAFE_PTR_DIFF(parser->m_bufferEnd, parser->m_bufferPtr); + parser->m_bufferPtr = parser->m_buffer; +#endif /* XML_CONTEXT_BYTES > 0 */ + } else { + int bufferSize + = (int)EXPAT_SAFE_PTR_DIFF(parser->m_bufferLim, parser->m_buffer); + if (bufferSize == 0) + bufferSize = INIT_BUFFER_SIZE; + do { + /* Do not invoke signed arithmetic overflow: */ + bufferSize = (int)(2U * (unsigned)bufferSize); + } while (bufferSize < neededSize && bufferSize > 0); + if (bufferSize <= 0) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + return NULL; + } + // NOTE: We are avoiding MALLOC(..) here to leave limiting + // the input size to the application using Expat. + char *const newBuf = parser->m_mem.malloc_fcn(bufferSize); + if (newBuf == NULL) { + parser->m_errorCode = XML_ERROR_NO_MEMORY; + return NULL; + } + setParserBuffer(parser, newBuf, bufferSize, keep); + } + parser->m_eventPtr = parser->m_eventEndPtr = NULL; + parser->m_positionPtr = NULL; + } + return parser->m_bufferEnd; +} + +static void +triggerReenter(XML_Parser parser) { + parser->m_reenter = XML_TRUE; +} + +enum XML_Status XMLCALL +XML_StopParser(XML_Parser parser, XML_Bool resumable) { + if (parser == NULL) + return XML_STATUS_ERROR; + switch (parser->m_parsingStatus.parsing) { + case XML_INITIALIZED: + parser->m_errorCode = XML_ERROR_NOT_STARTED; + return XML_STATUS_ERROR; + case XML_SUSPENDED: + if (resumable) { + parser->m_errorCode = XML_ERROR_SUSPENDED; + return XML_STATUS_ERROR; + } + parser->m_parsingStatus.parsing = XML_FINISHED; + break; + case XML_FINISHED: + parser->m_errorCode = XML_ERROR_FINISHED; + return XML_STATUS_ERROR; + case XML_PARSING: + if (resumable) { +#ifdef XML_DTD + if (parser->m_isParamEntity) { + parser->m_errorCode = XML_ERROR_SUSPEND_PE; + return XML_STATUS_ERROR; + } +#endif + parser->m_parsingStatus.parsing = XML_SUSPENDED; + } else + parser->m_parsingStatus.parsing = XML_FINISHED; + break; + default: + assert(0); + } + return XML_STATUS_OK; +} + +enum XML_Status XMLCALL +XML_ResumeParser(XML_Parser parser) { + enum XML_Status result = XML_STATUS_OK; + + if ((parser == NULL) || isCalledFromInsideHandler(parser)) + return XML_STATUS_ERROR; + if (parser->m_parsingStatus.parsing != XML_SUSPENDED) { + parser->m_errorCode = XML_ERROR_NOT_SUSPENDED; + return XML_STATUS_ERROR; + } + parser->m_parsingStatus.parsing = XML_PARSING; + + parser->m_errorCode = callProcessor( + parser, parser->m_bufferPtr, parser->m_parseEndPtr, &parser->m_bufferPtr); + + if (parser->m_errorCode != XML_ERROR_NONE) { + parser->m_eventEndPtr = parser->m_eventPtr; + parser->m_processor = errorProcessor; + return XML_STATUS_ERROR; + } else { + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + result = XML_STATUS_SUSPENDED; + break; + case XML_INITIALIZED: + case XML_PARSING: + if (parser->m_parsingStatus.finalBuffer) { + parser->m_parsingStatus.parsing = XML_FINISHED; + return result; + } + break; + default:; + } + } + + XmlUpdatePosition(parser->m_encoding, parser->m_positionPtr, + parser->m_bufferPtr, &parser->m_position); + parser->m_positionPtr = parser->m_bufferPtr; + return result; +} + +void XMLCALL +XML_GetParsingStatus(XML_Parser parser, XML_ParsingStatus *status) { + if (parser == NULL) + return; + assert(status != NULL); + *status = parser->m_parsingStatus; +} + +enum XML_Error XMLCALL +XML_GetErrorCode(XML_Parser parser) { + if (parser == NULL) + return XML_ERROR_INVALID_ARGUMENT; + return parser->m_errorCode; +} + +XML_Index XMLCALL +XML_GetCurrentByteIndex(XML_Parser parser) { + if (parser == NULL) + return -1; + if (parser->m_eventPtr) { + // NOTE: XML_Index is known to wrap around for >2 GiB content + // on 32bit machines and 64bit Windows, unless (non-default and + // uncommon) XML_LARGE_SIZE is defined. + // That's a bug and it only lives on because we cannot break + // ABI compatibility of public API. + return (XML_Index)(parser->m_parseEndByteIndex + - (parser->m_parseEndPtr - parser->m_eventPtr)); + } + return -1; +} + +int XMLCALL +XML_GetCurrentByteCount(XML_Parser parser) { + if (parser == NULL) + return 0; + if (parser->m_eventEndPtr && parser->m_eventPtr) + return (int)(parser->m_eventEndPtr - parser->m_eventPtr); + return 0; +} + +const char *XMLCALL +XML_GetInputContext(XML_Parser parser, int *offset, int *size) { +#if XML_CONTEXT_BYTES > 0 + if (parser == NULL) + return NULL; + if (parser->m_eventPtr && parser->m_buffer) { + if (offset != NULL) + *offset = (int)(parser->m_eventPtr - parser->m_buffer); + if (size != NULL) + *size = (int)(parser->m_bufferEnd - parser->m_buffer); + return parser->m_buffer; + } +#else + (void)parser; + (void)offset; + (void)size; +#endif /* XML_CONTEXT_BYTES > 0 */ + return NULL; +} + +XML_Size XMLCALL +XML_GetCurrentLineNumber(XML_Parser parser) { + if (parser == NULL) + return 0; + if (parser->m_eventPtr && parser->m_eventPtr >= parser->m_positionPtr) { + XmlUpdatePosition(parser->m_encoding, parser->m_positionPtr, + parser->m_eventPtr, &parser->m_position); + parser->m_positionPtr = parser->m_eventPtr; + } + // NOTE: XML_Size is known to wrap around for >4 GiB content + // on 32bit machines and 64bit Windows, unless (non-default and + // uncommon) XML_LARGE_SIZE is defined. + // That's a bug and it only lives on because we cannot break + // ABI compatibility of public API. + return (XML_Size)(parser->m_position.lineNumber + 1); +} + +XML_Size XMLCALL +XML_GetCurrentColumnNumber(XML_Parser parser) { + if (parser == NULL) + return 0; + if (parser->m_eventPtr && parser->m_eventPtr >= parser->m_positionPtr) { + XmlUpdatePosition(parser->m_encoding, parser->m_positionPtr, + parser->m_eventPtr, &parser->m_position); + parser->m_positionPtr = parser->m_eventPtr; + } + // NOTE: XML_Size is known to wrap around for >4 GiB content + // on 32bit machines and 64bit Windows, unless (non-default and + // uncommon) XML_LARGE_SIZE is defined. + // That's a bug and it only lives on because we cannot break + // ABI compatibility of public API. + return (XML_Size)parser->m_position.columnNumber; +} + +void XMLCALL +XML_FreeContentModel(XML_Parser parser, XML_Content *model) { + if (parser == NULL) + return; + + // NOTE: We are avoiding FREE(..) here because the content model + // has been created using plain .malloc_fcn(..) rather than MALLOC(..). + parser->m_mem.free_fcn(model); +} + +void *XMLCALL +XML_MemMalloc(XML_Parser parser, size_t size) { + if (parser == NULL) + return NULL; + + // NOTE: We are avoiding MALLOC(..) here to not include + // user allocations with allocation tracking and limiting. + return parser->m_mem.malloc_fcn(size); +} + +void *XMLCALL +XML_MemRealloc(XML_Parser parser, void *ptr, size_t size) { + if (parser == NULL) + return NULL; + + // NOTE: We are avoiding REALLOC(..) here to not include + // user allocations with allocation tracking and limiting. + return parser->m_mem.realloc_fcn(ptr, size); +} + +void XMLCALL +XML_MemFree(XML_Parser parser, void *ptr) { + if (parser == NULL) + return; + + // NOTE: We are avoiding FREE(..) here because XML_MemMalloc and + // XML_MemRealloc are not using MALLOC(..) and REALLOC(..) + // but plain .malloc_fcn(..) and .realloc_fcn(..), internally. + parser->m_mem.free_fcn(ptr); +} + +void XMLCALL +XML_DefaultCurrent(XML_Parser parser) { + if (parser == NULL) + return; + if (parser->m_defaultHandler) { + if (parser->m_openInternalEntities) + reportDefault(parser, parser->m_internalEncoding, + parser->m_openInternalEntities->internalEventPtr, + parser->m_openInternalEntities->internalEventEndPtr); + else + reportDefault(parser, parser->m_encoding, parser->m_eventPtr, + parser->m_eventEndPtr); + } +} + +const XML_LChar *XMLCALL +XML_ErrorString(enum XML_Error code) { + switch (code) { + case XML_ERROR_NONE: + return NULL; + case XML_ERROR_NO_MEMORY: + return XML_L("out of memory"); + case XML_ERROR_SYNTAX: + return XML_L("syntax error"); + case XML_ERROR_NO_ELEMENTS: + return XML_L("no element found"); + case XML_ERROR_INVALID_TOKEN: + return XML_L("not well-formed (invalid token)"); + case XML_ERROR_UNCLOSED_TOKEN: + return XML_L("unclosed token"); + case XML_ERROR_PARTIAL_CHAR: + return XML_L("partial character"); + case XML_ERROR_TAG_MISMATCH: + return XML_L("mismatched tag"); + case XML_ERROR_DUPLICATE_ATTRIBUTE: + return XML_L("duplicate attribute"); + case XML_ERROR_JUNK_AFTER_DOC_ELEMENT: + return XML_L("junk after document element"); + case XML_ERROR_PARAM_ENTITY_REF: + return XML_L("illegal parameter entity reference"); + case XML_ERROR_UNDEFINED_ENTITY: + return XML_L("undefined entity"); + case XML_ERROR_RECURSIVE_ENTITY_REF: + return XML_L("recursive entity reference"); + case XML_ERROR_ASYNC_ENTITY: + return XML_L("asynchronous entity"); + case XML_ERROR_BAD_CHAR_REF: + return XML_L("reference to invalid character number"); + case XML_ERROR_BINARY_ENTITY_REF: + return XML_L("reference to binary entity"); + case XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF: + return XML_L("reference to external entity in attribute"); + case XML_ERROR_MISPLACED_XML_PI: + return XML_L("XML or text declaration not at start of entity"); + case XML_ERROR_UNKNOWN_ENCODING: + return XML_L("unknown encoding"); + case XML_ERROR_INCORRECT_ENCODING: + return XML_L("encoding specified in XML declaration is incorrect"); + case XML_ERROR_UNCLOSED_CDATA_SECTION: + return XML_L("unclosed CDATA section"); + case XML_ERROR_EXTERNAL_ENTITY_HANDLING: + return XML_L("error in processing external entity reference"); + case XML_ERROR_NOT_STANDALONE: + return XML_L("document is not standalone"); + case XML_ERROR_UNEXPECTED_STATE: + return XML_L("unexpected parser state - please send a bug report"); + case XML_ERROR_ENTITY_DECLARED_IN_PE: + return XML_L("entity declared in parameter entity"); + case XML_ERROR_FEATURE_REQUIRES_XML_DTD: + return XML_L("requested feature requires XML_DTD support in Expat"); + case XML_ERROR_CANT_CHANGE_FEATURE_ONCE_PARSING: + return XML_L("cannot change setting once parsing has begun"); + /* Added in 1.95.7. */ + case XML_ERROR_UNBOUND_PREFIX: + return XML_L("unbound prefix"); + /* Added in 1.95.8. */ + case XML_ERROR_UNDECLARING_PREFIX: + return XML_L("must not undeclare prefix"); + case XML_ERROR_INCOMPLETE_PE: + return XML_L("incomplete markup in parameter entity"); + case XML_ERROR_XML_DECL: + return XML_L("XML declaration not well-formed"); + case XML_ERROR_TEXT_DECL: + return XML_L("text declaration not well-formed"); + case XML_ERROR_PUBLICID: + return XML_L("illegal character(s) in public id"); + case XML_ERROR_SUSPENDED: + return XML_L("parser suspended"); + case XML_ERROR_NOT_SUSPENDED: + return XML_L("parser not suspended"); + case XML_ERROR_ABORTED: + return XML_L("parsing aborted"); + case XML_ERROR_FINISHED: + return XML_L("parsing finished"); + case XML_ERROR_SUSPEND_PE: + return XML_L("cannot suspend in external parameter entity"); + /* Added in 2.0.0. */ + case XML_ERROR_RESERVED_PREFIX_XML: + return XML_L( + "reserved prefix (xml) must not be undeclared or bound to another namespace name"); + case XML_ERROR_RESERVED_PREFIX_XMLNS: + return XML_L("reserved prefix (xmlns) must not be declared or undeclared"); + case XML_ERROR_RESERVED_NAMESPACE_URI: + return XML_L( + "prefix must not be bound to one of the reserved namespace names"); + /* Added in 2.2.5. */ + case XML_ERROR_INVALID_ARGUMENT: /* Constant added in 2.2.1, already */ + return XML_L("invalid argument"); + /* Added in 2.3.0. */ + case XML_ERROR_NO_BUFFER: + return XML_L( + "a successful prior call to function XML_GetBuffer is required"); + /* Added in 2.4.0. */ + case XML_ERROR_AMPLIFICATION_LIMIT_BREACH: + return XML_L( + "limit on input amplification factor (from DTD and entities) breached"); + /* Added in 2.6.4. */ + case XML_ERROR_NOT_STARTED: + return XML_L("parser not started"); + } + return NULL; +} + +const XML_LChar *XMLCALL +XML_ExpatVersion(void) { + /* V1 is used to string-ize the version number. However, it would + string-ize the actual version macro *names* unless we get them + substituted before being passed to V1. CPP is defined to expand + a macro, then rescan for more expansions. Thus, we use V2 to expand + the version macros, then CPP will expand the resulting V1() macro + with the correct numerals. */ + /* ### I'm assuming cpp is portable in this respect... */ + +#define V1(a, b, c) XML_L(#a) XML_L(".") XML_L(#b) XML_L(".") XML_L(#c) +#define V2(a, b, c) XML_L("expat_") V1(a, b, c) + + return V2(XML_MAJOR_VERSION, XML_MINOR_VERSION, XML_MICRO_VERSION); + +#undef V1 +#undef V2 +} + +XML_Expat_Version XMLCALL +XML_ExpatVersionInfo(void) { + XML_Expat_Version version; + + version.major = XML_MAJOR_VERSION; + version.minor = XML_MINOR_VERSION; + version.micro = XML_MICRO_VERSION; + + return version; +} + +const XML_Feature *XMLCALL +XML_GetFeatureList(void) { + static const XML_Feature features[] = { + {XML_FEATURE_SIZEOF_XML_CHAR, XML_L("sizeof(XML_Char)"), + sizeof(XML_Char)}, + {XML_FEATURE_SIZEOF_XML_LCHAR, XML_L("sizeof(XML_LChar)"), + sizeof(XML_LChar)}, +#ifdef XML_UNICODE + {XML_FEATURE_UNICODE, XML_L("XML_UNICODE"), 0}, +#endif +#ifdef XML_UNICODE_WCHAR_T + {XML_FEATURE_UNICODE_WCHAR_T, XML_L("XML_UNICODE_WCHAR_T"), 0}, +#endif +#ifdef XML_DTD + {XML_FEATURE_DTD, XML_L("XML_DTD"), 0}, +#endif +#if XML_CONTEXT_BYTES > 0 + {XML_FEATURE_CONTEXT_BYTES, XML_L("XML_CONTEXT_BYTES"), + XML_CONTEXT_BYTES}, +#endif +#ifdef XML_MIN_SIZE + {XML_FEATURE_MIN_SIZE, XML_L("XML_MIN_SIZE"), 0}, +#endif +#ifdef XML_NS + {XML_FEATURE_NS, XML_L("XML_NS"), 0}, +#endif +#ifdef XML_LARGE_SIZE + {XML_FEATURE_LARGE_SIZE, XML_L("XML_LARGE_SIZE"), 0}, +#endif +#ifdef XML_ATTR_INFO + {XML_FEATURE_ATTR_INFO, XML_L("XML_ATTR_INFO"), 0}, +#endif +#if XML_GE == 1 + /* Added in Expat 2.4.0 for XML_DTD defined and + * added in Expat 2.6.0 for XML_GE == 1. */ + {XML_FEATURE_BILLION_LAUGHS_ATTACK_PROTECTION_MAXIMUM_AMPLIFICATION_DEFAULT, + XML_L("XML_BLAP_MAX_AMP"), + (long int) + EXPAT_BILLION_LAUGHS_ATTACK_PROTECTION_MAXIMUM_AMPLIFICATION_DEFAULT}, + {XML_FEATURE_BILLION_LAUGHS_ATTACK_PROTECTION_ACTIVATION_THRESHOLD_DEFAULT, + XML_L("XML_BLAP_ACT_THRES"), + EXPAT_BILLION_LAUGHS_ATTACK_PROTECTION_ACTIVATION_THRESHOLD_DEFAULT}, + /* Added in Expat 2.6.0. */ + {XML_FEATURE_GE, XML_L("XML_GE"), 0}, + /* Added in Expat 2.7.2. */ + {XML_FEATURE_ALLOC_TRACKER_MAXIMUM_AMPLIFICATION_DEFAULT, + XML_L("XML_AT_MAX_AMP"), + (long int)EXPAT_ALLOC_TRACKER_MAXIMUM_AMPLIFICATION_DEFAULT}, + {XML_FEATURE_ALLOC_TRACKER_ACTIVATION_THRESHOLD_DEFAULT, + XML_L("XML_AT_ACT_THRES"), + (long int)EXPAT_ALLOC_TRACKER_ACTIVATION_THRESHOLD_DEFAULT}, +#endif + {XML_FEATURE_END, NULL, 0}}; + + return features; +} + +#if XML_GE == 1 +XML_Bool XMLCALL +XML_SetBillionLaughsAttackProtectionMaximumAmplification( + XML_Parser parser, float maximumAmplificationFactor) { + if ((parser == NULL) || (parser->m_parentParser != NULL) + || isnan(maximumAmplificationFactor) + || (maximumAmplificationFactor < 1.0f)) { + return XML_FALSE; + } + parser->m_accounting.maximumAmplificationFactor = maximumAmplificationFactor; + return XML_TRUE; +} + +XML_Bool XMLCALL +XML_SetBillionLaughsAttackProtectionActivationThreshold( + XML_Parser parser, unsigned long long activationThresholdBytes) { + if ((parser == NULL) || (parser->m_parentParser != NULL)) { + return XML_FALSE; + } + parser->m_accounting.activationThresholdBytes = activationThresholdBytes; + return XML_TRUE; +} + +XML_Bool XMLCALL +XML_SetAllocTrackerMaximumAmplification(XML_Parser parser, + float maximumAmplificationFactor) { + if ((parser == NULL) || (parser->m_parentParser != NULL) + || isnan(maximumAmplificationFactor) + || (maximumAmplificationFactor < 1.0f)) { + return XML_FALSE; + } + parser->m_alloc_tracker.maximumAmplificationFactor + = maximumAmplificationFactor; + return XML_TRUE; +} + +XML_Bool XMLCALL +XML_SetAllocTrackerActivationThreshold( + XML_Parser parser, unsigned long long activationThresholdBytes) { + if ((parser == NULL) || (parser->m_parentParser != NULL)) { + return XML_FALSE; + } + parser->m_alloc_tracker.activationThresholdBytes = activationThresholdBytes; + return XML_TRUE; +} +#endif /* XML_GE == 1 */ + +XML_Bool XMLCALL +XML_SetReparseDeferralEnabled(XML_Parser parser, XML_Bool enabled) { + if (parser != NULL && (enabled == XML_TRUE || enabled == XML_FALSE)) { + parser->m_reparseDeferralEnabled = enabled; + return XML_TRUE; + } + return XML_FALSE; +} + +/* Initially tag->rawName always points into the parse buffer; + for those TAG instances opened while the current parse buffer was + processed, and not yet closed, we need to store tag->rawName in a more + permanent location, since the parse buffer is about to be discarded. +*/ +static XML_Bool +storeRawNames(XML_Parser parser) { + TAG *tag = parser->m_tagStack; + while (tag) { + size_t bufSize; + size_t nameLen = sizeof(XML_Char) * (tag->name.strLen + 1); + size_t rawNameLen; + char *rawNameBuf = tag->buf.raw + nameLen; + /* Stop if already stored. Since m_tagStack is a stack, we can stop + at the first entry that has already been copied; everything + below it in the stack is already been accounted for in a + previous call to this function. + */ + if (tag->rawName == rawNameBuf) + break; + /* For reuse purposes we need to ensure that the + size of tag->buf is a multiple of sizeof(XML_Char). + */ + rawNameLen = ROUND_UP(tag->rawNameLength, sizeof(XML_Char)); + /* Detect and prevent integer overflow. */ + if (rawNameLen > SIZE_MAX - nameLen) + return XML_FALSE; + bufSize = nameLen + rawNameLen; + if (bufSize > (size_t)(tag->bufEnd - tag->buf.raw)) { + char *temp = REALLOC(parser, tag->buf.raw, bufSize); + if (temp == NULL) + return XML_FALSE; + /* if tag->name.str points to tag->buf.str (only when namespace + processing is off) then we have to update it + */ + if (tag->name.str == tag->buf.str) + tag->name.str = (XML_Char *)temp; + /* if tag->name.localPart is set (when namespace processing is on) + then update it as well, since it will always point into tag->buf + */ + if (tag->name.localPart) + tag->name.localPart + = (XML_Char *)temp + (tag->name.localPart - tag->buf.str); + tag->buf.raw = temp; + tag->bufEnd = temp + bufSize; + rawNameBuf = temp + nameLen; + } + memcpy(rawNameBuf, tag->rawName, tag->rawNameLength); + tag->rawName = rawNameBuf; + tag = tag->parent; + } + return XML_TRUE; +} + +static enum XML_Error +contentProcessor(XML_Parser parser, const char *start, const char *end, + const char **endPtr) { + enum XML_Error result = doContent( + parser, parser->m_parentParser ? 1 : 0, parser->m_encoding, start, end, + endPtr, (XML_Bool)! parser->m_parsingStatus.finalBuffer, + XML_ACCOUNT_DIRECT); + if (result == XML_ERROR_NONE) { + if (! storeRawNames(parser)) + return XML_ERROR_NO_MEMORY; + } + return result; +} + +static enum XML_Error +externalEntityInitProcessor(XML_Parser parser, const char *start, + const char *end, const char **endPtr) { + enum XML_Error result = initializeEncoding(parser); + if (result != XML_ERROR_NONE) + return result; + parser->m_processor = externalEntityInitProcessor2; + return externalEntityInitProcessor2(parser, start, end, endPtr); +} + +static enum XML_Error +externalEntityInitProcessor2(XML_Parser parser, const char *start, + const char *end, const char **endPtr) { + const char *next = start; /* XmlContentTok doesn't always set the last arg */ + int tok = XmlContentTok(parser->m_encoding, start, end, &next); + switch (tok) { + case XML_TOK_BOM: +#if XML_GE == 1 + if (! accountingDiffTolerated(parser, tok, start, next, __LINE__, + XML_ACCOUNT_DIRECT)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } +#endif /* XML_GE == 1 */ + + /* If we are at the end of the buffer, this would cause the next stage, + i.e. externalEntityInitProcessor3, to pass control directly to + doContent (by detecting XML_TOK_NONE) without processing any xml text + declaration - causing the error XML_ERROR_MISPLACED_XML_PI in doContent. + */ + if (next == end && ! parser->m_parsingStatus.finalBuffer) { + *endPtr = next; + return XML_ERROR_NONE; + } + start = next; + break; + case XML_TOK_PARTIAL: + if (! parser->m_parsingStatus.finalBuffer) { + *endPtr = start; + return XML_ERROR_NONE; + } + parser->m_eventPtr = start; + return XML_ERROR_UNCLOSED_TOKEN; + case XML_TOK_PARTIAL_CHAR: + if (! parser->m_parsingStatus.finalBuffer) { + *endPtr = start; + return XML_ERROR_NONE; + } + parser->m_eventPtr = start; + return XML_ERROR_PARTIAL_CHAR; + } + parser->m_processor = externalEntityInitProcessor3; + return externalEntityInitProcessor3(parser, start, end, endPtr); +} + +static enum XML_Error +externalEntityInitProcessor3(XML_Parser parser, const char *start, + const char *end, const char **endPtr) { + int tok; + const char *next = start; /* XmlContentTok doesn't always set the last arg */ + parser->m_eventPtr = start; + tok = XmlContentTok(parser->m_encoding, start, end, &next); + /* Note: These bytes are accounted later in: + - processXmlDecl + - externalEntityContentProcessor + */ + parser->m_eventEndPtr = next; + + switch (tok) { + case XML_TOK_XML_DECL: { + enum XML_Error result; + result = processXmlDecl(parser, 1, start, next); + if (result != XML_ERROR_NONE) + return result; + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + *endPtr = next; + return XML_ERROR_NONE; + case XML_FINISHED: + return XML_ERROR_ABORTED; + case XML_PARSING: + if (parser->m_reenter) { + return XML_ERROR_UNEXPECTED_STATE; // LCOV_EXCL_LINE + } + EXPAT_FALLTHROUGH; + default: + start = next; + } + } break; + case XML_TOK_PARTIAL: + if (! parser->m_parsingStatus.finalBuffer) { + *endPtr = start; + return XML_ERROR_NONE; + } + return XML_ERROR_UNCLOSED_TOKEN; + case XML_TOK_PARTIAL_CHAR: + if (! parser->m_parsingStatus.finalBuffer) { + *endPtr = start; + return XML_ERROR_NONE; + } + return XML_ERROR_PARTIAL_CHAR; + } + parser->m_processor = externalEntityContentProcessor; + parser->m_tagLevel = 1; + return externalEntityContentProcessor(parser, start, end, endPtr); +} + +static enum XML_Error +externalEntityContentProcessor(XML_Parser parser, const char *start, + const char *end, const char **endPtr) { + enum XML_Error result + = doContent(parser, 1, parser->m_encoding, start, end, endPtr, + (XML_Bool)! parser->m_parsingStatus.finalBuffer, + XML_ACCOUNT_ENTITY_EXPANSION); + if (result == XML_ERROR_NONE) { + if (! storeRawNames(parser)) + return XML_ERROR_NO_MEMORY; + } + return result; +} + +static enum XML_Error +doContent(XML_Parser parser, int startTagLevel, const ENCODING *enc, + const char *s, const char *end, const char **nextPtr, + XML_Bool haveMore, enum XML_Account account) { + /* save one level of indirection */ + DTD *const dtd = parser->m_dtd; + + const char **eventPP; + const char **eventEndPP; + if (enc == parser->m_encoding) { + eventPP = &parser->m_eventPtr; + eventEndPP = &parser->m_eventEndPtr; + } else { + eventPP = &(parser->m_openInternalEntities->internalEventPtr); + eventEndPP = &(parser->m_openInternalEntities->internalEventEndPtr); + } + *eventPP = s; + + for (;;) { + const char *next = s; /* XmlContentTok doesn't always set the last arg */ + int tok = XmlContentTok(enc, s, end, &next); +#if XML_GE == 1 + const char *accountAfter + = ((tok == XML_TOK_TRAILING_RSQB) || (tok == XML_TOK_TRAILING_CR)) + ? (haveMore ? s /* i.e. 0 bytes */ : end) + : next; + if (! accountingDiffTolerated(parser, tok, s, accountAfter, __LINE__, + account)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } +#endif + *eventEndPP = next; + switch (tok) { + case XML_TOK_TRAILING_CR: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + *eventEndPP = end; + if (parser->m_characterDataHandler) { + XML_Char c = 0xA; + beforeHandler(parser); + parser->m_characterDataHandler(parser->m_handlerArg, &c, 1); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, end); + /* We are at the end of the final buffer, should we check for + XML_SUSPENDED, XML_FINISHED? + */ + if (startTagLevel == 0) + return XML_ERROR_NO_ELEMENTS; + if (parser->m_tagLevel != startTagLevel) + return XML_ERROR_ASYNC_ENTITY; + *nextPtr = end; + return XML_ERROR_NONE; + case XML_TOK_NONE: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + if (startTagLevel > 0) { + if (parser->m_tagLevel != startTagLevel) + return XML_ERROR_ASYNC_ENTITY; + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_NO_ELEMENTS; + case XML_TOK_INVALID: + *eventPP = next; + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_UNCLOSED_TOKEN; + case XML_TOK_PARTIAL_CHAR: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_PARTIAL_CHAR; + case XML_TOK_ENTITY_REF: { + const XML_Char *name; + ENTITY *entity; + XML_Char ch = (XML_Char)XmlPredefinedEntityName( + enc, s + enc->minBytesPerChar, next - enc->minBytesPerChar); + if (ch) { +#if XML_GE == 1 + /* NOTE: We are replacing 4-6 characters original input for 1 character + * so there is no amplification and hence recording without + * protection. */ + accountingDiffTolerated(parser, tok, (char *)&ch, + ((char *)&ch) + sizeof(XML_Char), __LINE__, + XML_ACCOUNT_ENTITY_EXPANSION); +#endif /* XML_GE == 1 */ + if (parser->m_characterDataHandler) { + beforeHandler(parser); + parser->m_characterDataHandler(parser->m_handlerArg, &ch, 1); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + break; + } + name = poolStoreString(&dtd->pool, enc, s + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! name) + return XML_ERROR_NO_MEMORY; + entity = (ENTITY *)lookup(parser, &dtd->generalEntities, name, 0); + poolDiscard(&dtd->pool); + /* First, determine if a check for an existing declaration is needed; + if yes, check that the entity exists, and that it is internal, + otherwise call the skipped entity or default handler. + */ + if (! dtd->hasParamEntityRefs || dtd->standalone) { + if (! entity) + return XML_ERROR_UNDEFINED_ENTITY; + else if (! entity->is_internal) + return XML_ERROR_ENTITY_DECLARED_IN_PE; + } else if (! entity) { + if (parser->m_skippedEntityHandler) { + beforeHandler(parser); + parser->m_skippedEntityHandler(parser->m_handlerArg, name, 0); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + break; + } + if (entity->open) + return XML_ERROR_RECURSIVE_ENTITY_REF; + if (entity->notation) + return XML_ERROR_BINARY_ENTITY_REF; + if (entity->textPtr) { + enum XML_Error result; + if (! parser->m_defaultExpandInternalEntities) { + if (parser->m_skippedEntityHandler) { + beforeHandler(parser); + parser->m_skippedEntityHandler(parser->m_handlerArg, entity->name, + 0); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + break; + } + result = processEntity(parser, entity, XML_FALSE, ENTITY_INTERNAL); + if (result != XML_ERROR_NONE) + return result; + } else if (parser->m_externalEntityRefHandler) { + const XML_Char *context; + entity->open = true; + context = getContext(parser); + entity->open = false; + if (! context) + return XML_ERROR_NO_MEMORY; + beforeHandler(parser); + const int status = parser->m_externalEntityRefHandler( + parser->m_externalEntityRefHandlerArg, context, entity->base, + entity->systemId, entity->publicId); + afterHandler(parser); + if (! status) + return XML_ERROR_EXTERNAL_ENTITY_HANDLING; + poolDiscard(&parser->m_tempPool); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + break; + } + case XML_TOK_START_TAG_NO_ATTS: + case XML_TOK_START_TAG_WITH_ATTS: { + TAG *tag; + enum XML_Error result; + XML_Char *toPtr; + if (parser->m_freeTagList) { + tag = parser->m_freeTagList; + parser->m_freeTagList = parser->m_freeTagList->parent; + } else { + tag = MALLOC(parser, sizeof(TAG)); + if (! tag) + return XML_ERROR_NO_MEMORY; + tag->buf.raw = MALLOC(parser, INIT_TAG_BUF_SIZE); + if (! tag->buf.raw) { + FREE(parser, tag); + return XML_ERROR_NO_MEMORY; + } + tag->bufEnd = tag->buf.raw + INIT_TAG_BUF_SIZE; + } + tag->bindings = NULL; + tag->parent = parser->m_tagStack; + parser->m_tagStack = tag; + tag->name.localPart = NULL; + tag->name.prefix = NULL; + tag->rawName = s + enc->minBytesPerChar; + tag->rawNameLength = XmlNameLength(enc, tag->rawName); + ++parser->m_tagLevel; + { + const char *rawNameEnd = tag->rawName + tag->rawNameLength; + const char *fromPtr = tag->rawName; + toPtr = tag->buf.str; + for (;;) { + const enum XML_Convert_Result convert_res + = XmlConvert(enc, &fromPtr, rawNameEnd, (ICHAR **)&toPtr, + (ICHAR *)tag->bufEnd - 1); + const size_t convLen = (size_t)(toPtr - tag->buf.str); + if ((fromPtr >= rawNameEnd) + || (convert_res == XML_CONVERT_INPUT_INCOMPLETE)) { + tag->name.strLen = convLen; + break; + } + if (SIZE_MAX / 2 < (size_t)(tag->bufEnd - tag->buf.raw)) + return XML_ERROR_NO_MEMORY; + const size_t bufSize = (size_t)(tag->bufEnd - tag->buf.raw) * 2; + { + char *temp = REALLOC(parser, tag->buf.raw, bufSize); + if (temp == NULL) + return XML_ERROR_NO_MEMORY; + tag->buf.raw = temp; + tag->bufEnd = temp + bufSize; + toPtr = (XML_Char *)temp + convLen; + } + } + } + tag->name.str = tag->buf.str; + *toPtr = XML_T('\0'); + result + = storeAtts(parser, enc, s, &(tag->name), &(tag->bindings), account); + if (result) + return result; + if (parser->m_startElementHandler) { + beforeHandler(parser); + parser->m_startElementHandler(parser->m_handlerArg, tag->name.str, + (const XML_Char **)parser->m_atts); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + poolClear(&parser->m_tempPool); + break; + } + case XML_TOK_EMPTY_ELEMENT_NO_ATTS: + case XML_TOK_EMPTY_ELEMENT_WITH_ATTS: { + const char *rawName = s + enc->minBytesPerChar; + enum XML_Error result; + BINDING *bindings = NULL; + XML_Bool noElmHandlers = XML_TRUE; + TAG_NAME name; + name.str = poolStoreString(&parser->m_tempPool, enc, rawName, + rawName + XmlNameLength(enc, rawName)); + if (! name.str) + return XML_ERROR_NO_MEMORY; + poolFinish(&parser->m_tempPool); + result = storeAtts(parser, enc, s, &name, &bindings, + XML_ACCOUNT_NONE /* token spans whole start tag */); + if (result != XML_ERROR_NONE) { + freeBindings(parser, bindings); + return result; + } + poolFinish(&parser->m_tempPool); + if (parser->m_startElementHandler) { + beforeHandler(parser); + parser->m_startElementHandler(parser->m_handlerArg, name.str, + (const XML_Char **)parser->m_atts); + afterHandler(parser); + noElmHandlers = XML_FALSE; + } + if (parser->m_endElementHandler) { + if (parser->m_startElementHandler) + *eventPP = *eventEndPP; + beforeHandler(parser); + parser->m_endElementHandler(parser->m_handlerArg, name.str); + afterHandler(parser); + noElmHandlers = XML_FALSE; + } + if (noElmHandlers && parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + poolClear(&parser->m_tempPool); + freeBindings(parser, bindings); + } + if ((parser->m_tagLevel == 0) + && (parser->m_parsingStatus.parsing != XML_FINISHED)) { + if (parser->m_parsingStatus.parsing == XML_SUSPENDED + || (parser->m_parsingStatus.parsing == XML_PARSING + && parser->m_reenter)) + parser->m_processor = epilogProcessor; + else + return epilogProcessor(parser, next, end, nextPtr); + } + break; + case XML_TOK_END_TAG: + if (parser->m_tagLevel == startTagLevel) + return XML_ERROR_ASYNC_ENTITY; + else { + int len; + const char *rawName; + TAG *tag = parser->m_tagStack; + rawName = s + enc->minBytesPerChar * 2; + len = XmlNameLength(enc, rawName); + if (len != tag->rawNameLength + || memcmp(tag->rawName, rawName, len) != 0) { + *eventPP = rawName; + return XML_ERROR_TAG_MISMATCH; + } + parser->m_tagStack = tag->parent; + tag->parent = parser->m_freeTagList; + parser->m_freeTagList = tag; + --parser->m_tagLevel; + if (parser->m_endElementHandler) { + const XML_Char *localPart; + const XML_Char *prefix; + XML_Char *uri; + localPart = tag->name.localPart; + if (parser->m_ns && localPart) { + /* localPart and prefix may have been overwritten in + tag->name.str, since this points to the binding->uri + buffer which gets reused; so we have to add them again + */ + uri = (XML_Char *)tag->name.str + tag->name.uriLen; + /* don't need to check for space - already done in storeAtts() */ + while (*localPart) + *uri++ = *localPart++; + prefix = tag->name.prefix; + if (parser->m_ns_triplets && prefix) { + *uri++ = parser->m_namespaceSeparator; + while (*prefix) + *uri++ = *prefix++; + } + *uri = XML_T('\0'); + } + beforeHandler(parser); + parser->m_endElementHandler(parser->m_handlerArg, tag->name.str); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + while (tag->bindings) { + BINDING *b = tag->bindings; + if (parser->m_endNamespaceDeclHandler) { + beforeHandler(parser); + parser->m_endNamespaceDeclHandler(parser->m_handlerArg, + b->prefix->name); + afterHandler(parser); + } + tag->bindings = tag->bindings->nextTagBinding; + b->nextTagBinding = parser->m_freeBindingList; + parser->m_freeBindingList = b; + b->prefix->binding = b->prevPrefixBinding; + } + if ((parser->m_tagLevel == 0) + && (parser->m_parsingStatus.parsing != XML_FINISHED)) { + if (parser->m_parsingStatus.parsing == XML_SUSPENDED + || (parser->m_parsingStatus.parsing == XML_PARSING + && parser->m_reenter)) + parser->m_processor = epilogProcessor; + else + return epilogProcessor(parser, next, end, nextPtr); + } + } + break; + case XML_TOK_CHAR_REF: { + int n = XmlCharRefNumber(enc, s); + if (n < 0) + return XML_ERROR_BAD_CHAR_REF; + if (parser->m_characterDataHandler) { + XML_Char buf[XML_ENCODE_MAX]; + beforeHandler(parser); + parser->m_characterDataHandler(parser->m_handlerArg, buf, + XmlEncode(n, (ICHAR *)buf)); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + } break; + case XML_TOK_XML_DECL: + return XML_ERROR_MISPLACED_XML_PI; + case XML_TOK_DATA_NEWLINE: + if (parser->m_characterDataHandler) { + XML_Char c = 0xA; + beforeHandler(parser); + parser->m_characterDataHandler(parser->m_handlerArg, &c, 1); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + break; + case XML_TOK_CDATA_SECT_OPEN: { + enum XML_Error result; + if (parser->m_startCdataSectionHandler) { + beforeHandler(parser); + parser->m_startCdataSectionHandler(parser->m_handlerArg); + afterHandler(parser); + /* BEGIN disabled code */ + /* Suppose you doing a transformation on a document that involves + changing only the character data. You set up a defaultHandler + and a characterDataHandler. The defaultHandler simply copies + characters through. The characterDataHandler does the + transformation and writes the characters out escaping them as + necessary. This case will fail to work if we leave out the + following two lines (because & and < inside CDATA sections will + be incorrectly escaped). + + However, now we have a start/endCdataSectionHandler, so it seems + easier to let the user deal with this. + */ + } else if ((0) && parser->m_characterDataHandler) { + beforeHandler(parser); + parser->m_characterDataHandler(parser->m_handlerArg, parser->m_dataBuf, + 0); + afterHandler(parser); + /* END disabled code */ + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + result + = doCdataSection(parser, enc, &next, end, nextPtr, haveMore, account); + if (result != XML_ERROR_NONE) + return result; + else if (! next) { + parser->m_processor = cdataSectionProcessor; + return result; + } + } break; + case XML_TOK_TRAILING_RSQB: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + if (parser->m_characterDataHandler) { + if (MUST_CONVERT(enc, s)) { + ICHAR *dataPtr = (ICHAR *)parser->m_dataBuf; + XmlConvert(enc, &s, end, &dataPtr, (ICHAR *)parser->m_dataBufEnd); + beforeHandler(parser); + parser->m_characterDataHandler( + parser->m_handlerArg, parser->m_dataBuf, + (int)(dataPtr - (ICHAR *)parser->m_dataBuf)); + afterHandler(parser); + } else { + beforeHandler(parser); + parser->m_characterDataHandler( + parser->m_handlerArg, (const XML_Char *)s, + (int)((const XML_Char *)end - (const XML_Char *)s)); + afterHandler(parser); + } + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, end); + /* We are at the end of the final buffer, should we check for + XML_SUSPENDED, XML_FINISHED? + */ + if (startTagLevel == 0) { + *eventPP = end; + return XML_ERROR_NO_ELEMENTS; + } + if (parser->m_tagLevel != startTagLevel) { + *eventPP = end; + return XML_ERROR_ASYNC_ENTITY; + } + *nextPtr = end; + return XML_ERROR_NONE; + case XML_TOK_DATA_CHARS: { + XML_CharacterDataHandler charDataHandler = parser->m_characterDataHandler; + if (charDataHandler) { + if (MUST_CONVERT(enc, s)) { + for (;;) { + ICHAR *dataPtr = (ICHAR *)parser->m_dataBuf; + const enum XML_Convert_Result convert_res = XmlConvert( + enc, &s, next, &dataPtr, (ICHAR *)parser->m_dataBufEnd); + *eventEndPP = s; + beforeHandler(parser); + charDataHandler(parser->m_handlerArg, parser->m_dataBuf, + (int)(dataPtr - (ICHAR *)parser->m_dataBuf)); + afterHandler(parser); + if ((convert_res == XML_CONVERT_COMPLETED) + || (convert_res == XML_CONVERT_INPUT_INCOMPLETE)) + break; + *eventPP = s; + } + } else { + beforeHandler(parser); + charDataHandler(parser->m_handlerArg, (const XML_Char *)s, + (int)((const XML_Char *)next - (const XML_Char *)s)); + afterHandler(parser); + } + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + } break; + case XML_TOK_PI: + if (! reportProcessingInstruction(parser, enc, s, next)) + return XML_ERROR_NO_MEMORY; + break; + case XML_TOK_COMMENT: + if (! reportComment(parser, enc, s, next)) + return XML_ERROR_NO_MEMORY; + break; + default: + /* All of the tokens produced by XmlContentTok() have their own + * explicit cases, so this default is not strictly necessary. + * However it is a useful safety net, so we retain the code and + * simply exclude it from the coverage tests. + * + * LCOV_EXCL_START + */ + if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + break; + /* LCOV_EXCL_STOP */ + } + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + *eventPP = next; + *nextPtr = next; + return XML_ERROR_NONE; + case XML_FINISHED: + *eventPP = next; + return XML_ERROR_ABORTED; + case XML_PARSING: + if (parser->m_reenter) { + *nextPtr = next; + return XML_ERROR_NONE; + } + EXPAT_FALLTHROUGH; + default:; + *eventPP = s = next; + } + } + /* not reached */ +} + +/* This function does not call free() on the allocated memory, merely + * moving it to the parser's m_freeBindingList where it can be freed or + * reused as appropriate. + */ +static void +freeBindings(XML_Parser parser, BINDING *bindings) { + while (bindings) { + BINDING *b = bindings; + + /* m_startNamespaceDeclHandler will have been called for this + * binding in addBindings(), so call the end handler now. + */ + if (parser->m_endNamespaceDeclHandler) { + beforeHandler(parser); + parser->m_endNamespaceDeclHandler(parser->m_handlerArg, b->prefix->name); + afterHandler(parser); + } + + bindings = bindings->nextTagBinding; + b->nextTagBinding = parser->m_freeBindingList; + parser->m_freeBindingList = b; + b->prefix->binding = b->prevPrefixBinding; + } +} + +/* Precondition: all arguments must be non-NULL; + Purpose: + - normalize attributes + - check attributes for well-formedness + - generate namespace aware attribute names (URI, prefix) + - build list of attributes for startElementHandler + - default attributes + - process namespace declarations (check and report them) + - generate namespace aware element name (URI, prefix) +*/ +static enum XML_Error +storeAtts(XML_Parser parser, const ENCODING *enc, const char *attStr, + TAG_NAME *tagNamePtr, BINDING **bindingsPtr, + enum XML_Account account) { + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + int attIndex = 0; + XML_Char *uri; + int nPrefixes = 0; + BINDING *binding; + const XML_Char *localPart; + + /* lookup the element type name */ + ELEMENT_TYPE *elementType + = (ELEMENT_TYPE *)lookup(parser, &dtd->elementTypes, tagNamePtr->str, 0); + if (! elementType) { + const XML_Char *name = poolCopyString(&dtd->pool, tagNamePtr->str); + if (! name) + return XML_ERROR_NO_MEMORY; + elementType = (ELEMENT_TYPE *)lookup(parser, &dtd->elementTypes, name, + sizeof(ELEMENT_TYPE)); + if (! elementType) + return XML_ERROR_NO_MEMORY; + if (! elementType->defaultAttForName.parser) + hashTableInit(&(elementType->defaultAttForName), parser); + if (parser->m_ns && ! setElementTypePrefix(parser, elementType)) + return XML_ERROR_NO_MEMORY; + } + const size_t nDefaultAtts = elementType->nDefaultAtts; + + /* Detect and prevent integer overflow. */ + if (parser->m_attsSize > (size_t)INT_MAX) + return XML_ERROR_NO_MEMORY; + + /* get the attributes from the tokenizer */ + size_t n = (size_t)XmlGetAttributes(enc, attStr, (int)parser->m_attsSize, + parser->m_atts); + + /* Detect and prevent integer overflow */ + if (n > SIZE_MAX - nDefaultAtts) { + return XML_ERROR_NO_MEMORY; + } + + if (n + nDefaultAtts > parser->m_attsSize) { + size_t oldAttsSize = parser->m_attsSize; + + /* Detect and prevent integer overflow */ + if ((nDefaultAtts > SIZE_MAX - INIT_ATTS_SIZE) + || (n > SIZE_MAX - (nDefaultAtts + INIT_ATTS_SIZE))) { + return XML_ERROR_NO_MEMORY; + } + + parser->m_attsSize = n + nDefaultAtts + INIT_ATTS_SIZE; + + /* Detect and prevent integer overflow. */ + if (parser->m_attsSize > SIZE_MAX / sizeof(ATTRIBUTE)) { + parser->m_attsSize = oldAttsSize; + return XML_ERROR_NO_MEMORY; + } + + ATTRIBUTE *const temp = REALLOC(parser, parser->m_atts, + parser->m_attsSize * sizeof(ATTRIBUTE)); + if (temp == NULL) { + parser->m_attsSize = oldAttsSize; + return XML_ERROR_NO_MEMORY; + } + parser->m_atts = temp; +#ifdef XML_ATTR_INFO + /* Detect and prevent integer overflow. */ + if (parser->m_attsSize > SIZE_MAX / sizeof(XML_AttrInfo)) { + parser->m_attsSize = oldAttsSize; + return XML_ERROR_NO_MEMORY; + } + + XML_AttrInfo *const temp2 = REALLOC( + parser, parser->m_attInfo, parser->m_attsSize * sizeof(XML_AttrInfo)); + if (temp2 == NULL) { + parser->m_attsSize = oldAttsSize; + return XML_ERROR_NO_MEMORY; + } + parser->m_attInfo = temp2; +#endif + if (n > oldAttsSize) { + /* Detect and prevent integer overflow. */ + if (n > (size_t)INT_MAX) + return XML_ERROR_NO_MEMORY; + XmlGetAttributes(enc, attStr, (int)n, parser->m_atts); + } + } + + /* the attribute list for the application */ + const XML_Char **const appAtts = (const XML_Char **)parser->m_atts; + for (size_t i = 0; i < n; i++) { + ATTRIBUTE *currAtt = &parser->m_atts[i]; +#ifdef XML_ATTR_INFO + XML_AttrInfo *currAttInfo = &parser->m_attInfo[i]; +#endif + /* add the name and value to the attribute list */ + ATTRIBUTE_ID *attId + = getAttributeId(parser, enc, currAtt->name, + currAtt->name + XmlNameLength(enc, currAtt->name)); + if (! attId) + return XML_ERROR_NO_MEMORY; +#ifdef XML_ATTR_INFO + // NOTE: XML_Index is known to wrap around for >2 GiB content + // on 32bit machines and 64bit Windows, unless (non-default and + // uncommon) XML_LARGE_SIZE is defined. + // That's a bug and it only lives on because we cannot break + // ABI compatibility of public API. + currAttInfo->nameStart + = (XML_Index)(parser->m_parseEndByteIndex + - (parser->m_parseEndPtr - currAtt->name)); + currAttInfo->nameEnd + = currAttInfo->nameStart + XmlNameLength(enc, currAtt->name); + currAttInfo->valueStart + = (XML_Index)(parser->m_parseEndByteIndex + - (parser->m_parseEndPtr - currAtt->valuePtr)); + currAttInfo->valueEnd + = (XML_Index)(parser->m_parseEndByteIndex + - (parser->m_parseEndPtr - currAtt->valueEnd)); +#endif + /* Detect duplicate attributes by their QNames. This does not work when + namespace processing is turned on and different prefixes for the same + namespace are used. For this case we have a check further down. + */ + if ((attId->name)[-1]) { + if (enc == parser->m_encoding) + parser->m_eventPtr = parser->m_atts[i].name; + return XML_ERROR_DUPLICATE_ATTRIBUTE; + } + (attId->name)[-1] = 1; + appAtts[attIndex++] = attId->name; + if (! parser->m_atts[i].normalized) { + XML_Bool isCdata = XML_TRUE; + + /* figure out whether declared as other than CDATA */ + if (attId->maybeTokenized) { + NAME_AND_DEFAULT_ATTRIBUTE *const nameAndDefaultAttribute + = (NAME_AND_DEFAULT_ATTRIBUTE *)lookup( + parser, &(elementType->defaultAttForName), attId->name, 0); + if (nameAndDefaultAttribute != NULL) { + assert(nameAndDefaultAttribute->attIndex < elementType->nDefaultAtts); + const DEFAULT_ATTRIBUTE *const att + = elementType->defaultAtts + nameAndDefaultAttribute->attIndex; + isCdata = att->isCdata; + } + } + + /* normalize the attribute value */ + const enum XML_Error result = storeAttributeValue( + parser, enc, isCdata, parser->m_atts[i].valuePtr, + parser->m_atts[i].valueEnd, &parser->m_tempPool, account); + if (result) + return result; + appAtts[attIndex] = poolStart(&parser->m_tempPool); + poolFinish(&parser->m_tempPool); + } else { + /* the value did not need normalizing */ + appAtts[attIndex] = poolStoreString(&parser->m_tempPool, enc, + parser->m_atts[i].valuePtr, + parser->m_atts[i].valueEnd); + if (appAtts[attIndex] == 0) + return XML_ERROR_NO_MEMORY; + poolFinish(&parser->m_tempPool); + } + /* handle prefixed attribute names */ + if (attId->prefix) { + if (attId->xmlns) { + /* deal with namespace declarations here */ + enum XML_Error result = addBinding(parser, attId->prefix, attId, + appAtts[attIndex], bindingsPtr); + if (result) + return result; + --attIndex; + } else { + /* deal with other prefixed names later */ + attIndex++; + nPrefixes++; + (attId->name)[-1] = 2; + } + } else + attIndex++; + } + + /* set-up for XML_GetSpecifiedAttributeCount and XML_GetIdAttributeIndex */ + parser->m_nSpecifiedAtts = attIndex; + if (elementType->idAtt && (elementType->idAtt->name)[-1]) { + for (int i = 0; i < attIndex; i += 2) + if (appAtts[i] == elementType->idAtt->name) { + parser->m_idAttIndex = i; + break; + } + } else + parser->m_idAttIndex = -1; + + /* do attribute defaulting */ + for (size_t i = 0; i < nDefaultAtts; i++) { + const DEFAULT_ATTRIBUTE *da = elementType->defaultAtts + i; + if (! (da->id->name)[-1] && da->value) { + if (da->id->prefix) { + if (da->id->xmlns) { + enum XML_Error result = addBinding(parser, da->id->prefix, da->id, + da->value, bindingsPtr); + if (result) + return result; + } else { + (da->id->name)[-1] = 2; + nPrefixes++; + appAtts[attIndex++] = da->id->name; + appAtts[attIndex++] = da->value; + } + } else { + (da->id->name)[-1] = 1; + appAtts[attIndex++] = da->id->name; + appAtts[attIndex++] = da->value; + } + } + } + appAtts[attIndex] = 0; + + /* expand prefixed attribute names, check for duplicates, + and clear flags that say whether attributes were specified */ + int i = 0; + if (nPrefixes) { + unsigned int j; /* hash table index */ + unsigned long version = parser->m_nsAttsVersion; + + /* Detect and prevent invalid shift */ + if (parser->m_nsAttsPower >= sizeof(unsigned int) * 8 /* bits per byte */) { + return XML_ERROR_NO_MEMORY; + } + + unsigned int nsAttsSize = 1u << parser->m_nsAttsPower; + unsigned char oldNsAttsPower = parser->m_nsAttsPower; + /* size of hash table must be at least 2 * (# of prefixed attributes) */ + if (parser->m_nsAttsPower == 0 + || (nPrefixes >> (parser->m_nsAttsPower - 1))) { + /* hash table size must also be a power of 2 and >= 8 */ + while (nPrefixes >> parser->m_nsAttsPower++) + ; + if (parser->m_nsAttsPower < 3) + parser->m_nsAttsPower = 3; + + /* Detect and prevent invalid shift */ + if (parser->m_nsAttsPower >= sizeof(nsAttsSize) * 8 /* bits per byte */) { + /* Restore actual size of memory in m_nsAtts */ + parser->m_nsAttsPower = oldNsAttsPower; + return XML_ERROR_NO_MEMORY; + } + + nsAttsSize = 1u << parser->m_nsAttsPower; + + /* Detect and prevent integer overflow. + * The preprocessor guard addresses the "always false" warning + * from -Wtype-limits on platforms where + * sizeof(unsigned int) < sizeof(size_t), e.g. on x86_64. */ +#if UINT_MAX >= SIZE_MAX + if (nsAttsSize > SIZE_MAX / sizeof(NS_ATT)) { + /* Restore actual size of memory in m_nsAtts */ + parser->m_nsAttsPower = oldNsAttsPower; + return XML_ERROR_NO_MEMORY; + } +#endif + + NS_ATT *const temp + = REALLOC(parser, parser->m_nsAtts, nsAttsSize * sizeof(NS_ATT)); + if (! temp) { + /* Restore actual size of memory in m_nsAtts */ + parser->m_nsAttsPower = oldNsAttsPower; + return XML_ERROR_NO_MEMORY; + } + parser->m_nsAtts = temp; + version = 0; /* force re-initialization of m_nsAtts hash table */ + } + /* using a version flag saves us from initializing m_nsAtts every time */ + if (! version) { /* initialize version flags when version wraps around */ + version = INIT_ATTS_VERSION; + for (j = nsAttsSize; j != 0;) + parser->m_nsAtts[--j].version = version; + } + parser->m_nsAttsVersion = --version; + + /* expand prefixed names and check for duplicates */ + for (; i < attIndex; i += 2) { + const XML_Char *s = appAtts[i]; + if (s[-1] == 2) { /* prefixed */ + struct siphash sip_state; + struct sipkey sip_key; + + copy_salt_to_sipkey(parser, &sip_key); + sip24_init(&sip_state, &sip_key); + + ((XML_Char *)s)[-1] = 0; /* clear flag */ + ATTRIBUTE_ID *const id + = (ATTRIBUTE_ID *)lookup(parser, &dtd->attributeIds, s, 0); + if (! id || ! id->prefix) { + /* This code is walking through the appAtts array, dealing + * with (in this case) a prefixed attribute name. To be in + * the array, the attribute must have already been bound, so + * has to have passed through the hash table lookup once + * already. That implies that an entry for it already + * exists, so the lookup above will return a pointer to + * already allocated memory. There is no opportunity for + * the allocator to fail, so the condition above cannot be + * fulfilled. + * + * Since it is difficult to be certain that the above + * analysis is complete, we retain the test and merely + * remove the code from coverage tests. + */ + return XML_ERROR_NO_MEMORY; /* LCOV_EXCL_LINE */ + } + const BINDING *const b = id->prefix->binding; + if (! b) + return XML_ERROR_UNBOUND_PREFIX; + + if (! poolAppendChars(&parser->m_tempPool, b->uri, b->uriLen)) + return XML_ERROR_NO_MEMORY; + + sip24_update(&sip_state, b->uri, b->uriLen * sizeof(XML_Char)); + + while (*s++ != XML_T(ASCII_COLON)) + ; + + sip24_update(&sip_state, s, keylen(s) * sizeof(XML_Char)); + + { + const size_t len = xcslen(s) + /*null terminator*/ 1; + if (! poolAppendChars(&parser->m_tempPool, s, len)) + return XML_ERROR_NO_MEMORY; + } + + const unsigned long uriHash = (unsigned long)sip24_final(&sip_state); + + { /* Check hash table for duplicate of expanded name (uriName). + Derived from code in lookup(parser, HASH_TABLE *table, ...). + */ + unsigned char step = 0; + unsigned long mask = nsAttsSize - 1; + j = uriHash & mask; /* index into hash table */ + while (parser->m_nsAtts[j].version == version) { + /* for speed we compare stored hash values first */ + if (uriHash == parser->m_nsAtts[j].hash) { + const XML_Char *s1 = poolStart(&parser->m_tempPool); + const XML_Char *s2 = parser->m_nsAtts[j].uriName; + /* s1 is null terminated, but not s2 */ + for (; *s1 == *s2 && *s1 != 0; s1++, s2++) + ; + if (*s1 == 0) + return XML_ERROR_DUPLICATE_ATTRIBUTE; + } + if (! step) + step = PROBE_STEP(uriHash, mask, parser->m_nsAttsPower); + j < step ? (j += nsAttsSize - step) : (j -= step); + } + } + + if (parser->m_ns_triplets) { /* append namespace separator and prefix */ + parser->m_tempPool.ptr[-1] = parser->m_namespaceSeparator; + s = b->prefix->name; + const size_t len = xcslen(s) + /*null terminator*/ 1; + if (! poolAppendChars(&parser->m_tempPool, s, len)) + return XML_ERROR_NO_MEMORY; + } + + /* store expanded name in attribute list */ + s = poolStart(&parser->m_tempPool); + poolFinish(&parser->m_tempPool); + appAtts[i] = s; + + /* fill empty slot with new version, uriName and hash value */ + parser->m_nsAtts[j].version = version; + parser->m_nsAtts[j].hash = uriHash; + parser->m_nsAtts[j].uriName = s; + + if (! --nPrefixes) { + i += 2; + break; + } + } else /* not prefixed */ + ((XML_Char *)s)[-1] = 0; /* clear flag */ + } + } + /* clear flags for the remaining attributes */ + for (; i < attIndex; i += 2) + ((XML_Char *)(appAtts[i]))[-1] = 0; + for (binding = *bindingsPtr; binding; binding = binding->nextTagBinding) + binding->attId->name[-1] = 0; + + if (! parser->m_ns) + return XML_ERROR_NONE; + + /* expand the element type name */ + if (elementType->prefix) { + binding = elementType->prefix->binding; + if (! binding) + return XML_ERROR_UNBOUND_PREFIX; + localPart = tagNamePtr->str; + while (*localPart++ != XML_T(ASCII_COLON)) + ; + } else if (dtd->defaultPrefix.binding) { + binding = dtd->defaultPrefix.binding; + localPart = tagNamePtr->str; + } else + return XML_ERROR_NONE; + size_t prefixLen = 0; + if (parser->m_ns_triplets && binding->prefix->name) + prefixLen = xcslen(binding->prefix->name) + /*null terminator*/ 1; + tagNamePtr->localPart = localPart; + tagNamePtr->uriLen = binding->uriLen; + tagNamePtr->prefix = binding->prefix->name; + tagNamePtr->prefixLen = prefixLen; + + const size_t localPartLen = xcslen(localPart) + /*null terminator*/ 1; + + /* Detect and prevent integer overflow */ + if (binding->uriLen > SIZE_MAX - prefixLen + || localPartLen > SIZE_MAX - (binding->uriLen + prefixLen)) { + return XML_ERROR_NO_MEMORY; + } + + const size_t totalLen = localPartLen + binding->uriLen + prefixLen; + if (totalLen > binding->uriAlloc) { + /* Detect and prevent integer overflow */ + if (totalLen > SIZE_MAX - EXPAND_SPARE + || totalLen + EXPAND_SPARE > SIZE_MAX / sizeof(XML_Char)) { + return XML_ERROR_NO_MEMORY; + } + + uri = MALLOC(parser, (totalLen + EXPAND_SPARE) * sizeof(XML_Char)); + if (! uri) + return XML_ERROR_NO_MEMORY; + binding->uriAlloc = totalLen + EXPAND_SPARE; + memcpy(uri, binding->uri, binding->uriLen * sizeof(XML_Char)); + for (TAG *p = parser->m_tagStack; p; p = p->parent) + if (p->name.str == binding->uri) + p->name.str = uri; + FREE(parser, binding->uri); + binding->uri = uri; + } + /* if m_namespaceSeparator != '\0' then uri includes it already */ + uri = binding->uri + binding->uriLen; + /* Detect and prevent integer overflow */ + if (localPartLen > SIZE_MAX / sizeof(XML_Char)) { + return XML_ERROR_NO_MEMORY; + } + memcpy(uri, localPart, localPartLen * sizeof(XML_Char)); + /* we always have a namespace separator between localPart and prefix */ + if (prefixLen) { + uri += localPartLen - 1; + *uri = parser->m_namespaceSeparator; /* replace null terminator */ + memcpy(uri + 1, binding->prefix->name, prefixLen * sizeof(XML_Char)); + } + tagNamePtr->str = binding->uri; + return XML_ERROR_NONE; +} + +static XML_Bool +is_rfc3986_uri_char(XML_Char candidate) { + // For the RFC 3986 ANBF grammar see + // https://datatracker.ietf.org/doc/html/rfc3986#appendix-A + + switch (candidate) { + // From rule "ALPHA" (uppercase half) + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'H': + case 'I': + case 'J': + case 'K': + case 'L': + case 'M': + case 'N': + case 'O': + case 'P': + case 'Q': + case 'R': + case 'S': + case 'T': + case 'U': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'Z': + + // From rule "ALPHA" (lowercase half) + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'g': + case 'h': + case 'i': + case 'j': + case 'k': + case 'l': + case 'm': + case 'n': + case 'o': + case 'p': + case 'q': + case 'r': + case 's': + case 't': + case 'u': + case 'v': + case 'w': + case 'x': + case 'y': + case 'z': + + // From rule "DIGIT" + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + + // From rule "pct-encoded" + case '%': + + // From rule "unreserved" + case '-': + case '.': + case '_': + case '~': + + // From rule "gen-delims" + case ':': + case '/': + case '?': + case '#': + case '[': + case ']': + case '@': + + // From rule "sub-delims" + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + return XML_TRUE; + + default: + return XML_FALSE; + } +} + +/* addBinding() overwrites the value of prefix->binding without checking. + Therefore one must keep track of the old value outside of addBinding(). +*/ +static enum XML_Error +addBinding(XML_Parser parser, PREFIX *prefix, const ATTRIBUTE_ID *attId, + const XML_Char *uri, BINDING **bindingsPtr) { + // "http://www.w3.org/XML/1998/namespace" + static const XML_Char xmlNamespace[] + = {ASCII_h, ASCII_t, ASCII_t, ASCII_p, ASCII_COLON, + ASCII_SLASH, ASCII_SLASH, ASCII_w, ASCII_w, ASCII_w, + ASCII_PERIOD, ASCII_w, ASCII_3, ASCII_PERIOD, ASCII_o, + ASCII_r, ASCII_g, ASCII_SLASH, ASCII_X, ASCII_M, + ASCII_L, ASCII_SLASH, ASCII_1, ASCII_9, ASCII_9, + ASCII_8, ASCII_SLASH, ASCII_n, ASCII_a, ASCII_m, + ASCII_e, ASCII_s, ASCII_p, ASCII_a, ASCII_c, + ASCII_e, '\0'}; + static const size_t xmlLen = sizeof(xmlNamespace) / sizeof(XML_Char) - 1; + // "http://www.w3.org/2000/xmlns/" + static const XML_Char xmlnsNamespace[] + = {ASCII_h, ASCII_t, ASCII_t, ASCII_p, ASCII_COLON, ASCII_SLASH, + ASCII_SLASH, ASCII_w, ASCII_w, ASCII_w, ASCII_PERIOD, ASCII_w, + ASCII_3, ASCII_PERIOD, ASCII_o, ASCII_r, ASCII_g, ASCII_SLASH, + ASCII_2, ASCII_0, ASCII_0, ASCII_0, ASCII_SLASH, ASCII_x, + ASCII_m, ASCII_l, ASCII_n, ASCII_s, ASCII_SLASH, '\0'}; + static const size_t xmlnsLen = sizeof(xmlnsNamespace) / sizeof(XML_Char) - 1; + + XML_Bool mustBeXML = XML_FALSE; + XML_Bool isXML = XML_TRUE; + XML_Bool isXMLNS = XML_TRUE; + + BINDING *b; + size_t len; + + /* empty URI is only valid for default namespace per XML NS 1.0 (not 1.1) */ + if (*uri == XML_T('\0') && prefix->name) + return XML_ERROR_UNDECLARING_PREFIX; + + if (prefix->name && prefix->name[0] == XML_T(ASCII_x) + && prefix->name[1] == XML_T(ASCII_m) + && prefix->name[2] == XML_T(ASCII_l)) { + /* Not allowed to bind xmlns */ + if (prefix->name[3] == XML_T(ASCII_n) && prefix->name[4] == XML_T(ASCII_s) + && prefix->name[5] == XML_T('\0')) + return XML_ERROR_RESERVED_PREFIX_XMLNS; + + if (prefix->name[3] == XML_T('\0')) + mustBeXML = XML_TRUE; + } + + for (len = 0; uri[len]; len++) { + /* Detect and prevent integer overflow */ + if (len == SIZE_MAX) { + return XML_ERROR_NO_MEMORY; + } + if (isXML && (len > xmlLen || uri[len] != xmlNamespace[len])) + isXML = XML_FALSE; + + if (! mustBeXML && isXMLNS + && (len > xmlnsLen || uri[len] != xmlnsNamespace[len])) + isXMLNS = XML_FALSE; + + // NOTE: While Expat does not validate namespace URIs against RFC 3986 + // today (and is not REQUIRED to do so with regard to the XML 1.0 + // namespaces specification) we have to at least make sure, that + // the application on top of Expat (that is likely splitting expanded + // element names ("qualified names") of form + // "[uri sep] local [sep prefix] '\0'" back into 1, 2 or 3 pieces + // in its element handler code) cannot be confused by an attacker + // putting additional namespace separator characters into namespace + // declarations. That would be ambiguous and not to be expected. + // + // While the HTML API docs of function XML_ParserCreateNS have been + // advising against use of a namespace separator character that can + // appear in a URI for >20 years now, some widespread applications + // are using URI characters (':' (colon) in particular) for a + // namespace separator, in practice. To keep these applications + // functional, we only reject namespaces URIs containing the + // application-chosen namespace separator if the chosen separator + // is a non-URI character with regard to RFC 3986. + if (parser->m_ns && (uri[len] == parser->m_namespaceSeparator) + && ! is_rfc3986_uri_char(uri[len])) { + return XML_ERROR_SYNTAX; + } + } + isXML = isXML && len == xmlLen; + isXMLNS = isXMLNS && len == xmlnsLen; + + if (mustBeXML != isXML) + return mustBeXML ? XML_ERROR_RESERVED_PREFIX_XML + : XML_ERROR_RESERVED_NAMESPACE_URI; + + if (isXMLNS) + return XML_ERROR_RESERVED_NAMESPACE_URI; + + if (parser->m_namespaceSeparator) { + /* Detect and prevent integer overflow */ + if (len == SIZE_MAX) { + return XML_ERROR_NO_MEMORY; + } + len++; + } + if (parser->m_freeBindingList) { + b = parser->m_freeBindingList; + if (len > b->uriAlloc) { + /* Detect and prevent integer overflow */ + if (len > SIZE_MAX - EXPAND_SPARE + || len + EXPAND_SPARE > SIZE_MAX / sizeof(XML_Char)) { + return XML_ERROR_NO_MEMORY; + } + + XML_Char *temp + = REALLOC(parser, b->uri, sizeof(XML_Char) * (len + EXPAND_SPARE)); + if (temp == NULL) + return XML_ERROR_NO_MEMORY; + b->uri = temp; + b->uriAlloc = len + EXPAND_SPARE; + } + parser->m_freeBindingList = b->nextTagBinding; + } else { + b = MALLOC(parser, sizeof(BINDING)); + if (! b) + return XML_ERROR_NO_MEMORY; + + /* Detect and prevent integer overflow */ + if (len > SIZE_MAX - EXPAND_SPARE + || len + EXPAND_SPARE > SIZE_MAX / sizeof(XML_Char)) { + FREE(parser, b); + return XML_ERROR_NO_MEMORY; + } + + b->uri = MALLOC(parser, sizeof(XML_Char) * (len + EXPAND_SPARE)); + if (! b->uri) { + FREE(parser, b); + return XML_ERROR_NO_MEMORY; + } + b->uriAlloc = len + EXPAND_SPARE; + } + b->uriLen = len; + memcpy(b->uri, uri, len * sizeof(XML_Char)); + if (parser->m_namespaceSeparator) + b->uri[len - 1] = parser->m_namespaceSeparator; + b->prefix = prefix; + b->attId = attId; + b->prevPrefixBinding = prefix->binding; + /* NULL binding when default namespace undeclared */ + if (*uri == XML_T('\0') && prefix == &parser->m_dtd->defaultPrefix) + prefix->binding = NULL; + else + prefix->binding = b; + b->nextTagBinding = *bindingsPtr; + *bindingsPtr = b; + /* if attId == NULL then we are not starting a namespace scope */ + if (attId && parser->m_startNamespaceDeclHandler) { + beforeHandler(parser); + parser->m_startNamespaceDeclHandler(parser->m_handlerArg, prefix->name, + prefix->binding ? uri : 0); + afterHandler(parser); + } + return XML_ERROR_NONE; +} + +/* The idea here is to avoid using stack for each CDATA section when + the whole file is parsed with one call. +*/ +static enum XML_Error +cdataSectionProcessor(XML_Parser parser, const char *start, const char *end, + const char **endPtr) { + enum XML_Error result = doCdataSection( + parser, parser->m_encoding, &start, end, endPtr, + (XML_Bool)! parser->m_parsingStatus.finalBuffer, XML_ACCOUNT_DIRECT); + if (result != XML_ERROR_NONE) + return result; + if (start) { + if (parser->m_parentParser) { /* we are parsing an external entity */ + parser->m_processor = externalEntityContentProcessor; + return externalEntityContentProcessor(parser, start, end, endPtr); + } else { + parser->m_processor = contentProcessor; + return contentProcessor(parser, start, end, endPtr); + } + } + return result; +} + +/* startPtr gets set to non-null if the section is closed, and to null if + the section is not yet closed. +*/ +static enum XML_Error +doCdataSection(XML_Parser parser, const ENCODING *enc, const char **startPtr, + const char *end, const char **nextPtr, XML_Bool haveMore, + enum XML_Account account) { + const char *s = *startPtr; + const char **eventPP; + const char **eventEndPP; + if (enc == parser->m_encoding) { + eventPP = &parser->m_eventPtr; + *eventPP = s; + eventEndPP = &parser->m_eventEndPtr; + } else { + eventPP = &(parser->m_openInternalEntities->internalEventPtr); + eventEndPP = &(parser->m_openInternalEntities->internalEventEndPtr); + } + *eventPP = s; + *startPtr = NULL; + + for (;;) { + const char *next = s; /* in case of XML_TOK_NONE or XML_TOK_PARTIAL */ + int tok = XmlCdataSectionTok(enc, s, end, &next); +#if XML_GE == 1 + if (! accountingDiffTolerated(parser, tok, s, next, __LINE__, account)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } +#else + UNUSED_P(account); +#endif + *eventEndPP = next; + switch (tok) { + case XML_TOK_CDATA_SECT_CLOSE: + if (parser->m_endCdataSectionHandler) { + beforeHandler(parser); + parser->m_endCdataSectionHandler(parser->m_handlerArg); + afterHandler(parser); + } + /* BEGIN disabled code */ + /* see comment under XML_TOK_CDATA_SECT_OPEN */ + else if ((0) && parser->m_characterDataHandler) { + beforeHandler(parser); + parser->m_characterDataHandler(parser->m_handlerArg, parser->m_dataBuf, + 0); + afterHandler(parser); + /* END disabled code */ + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + *startPtr = next; + *nextPtr = next; + if (parser->m_parsingStatus.parsing == XML_FINISHED) + return XML_ERROR_ABORTED; + else + return XML_ERROR_NONE; + case XML_TOK_DATA_NEWLINE: + if (parser->m_characterDataHandler) { + XML_Char c = 0xA; + beforeHandler(parser); + parser->m_characterDataHandler(parser->m_handlerArg, &c, 1); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + break; + case XML_TOK_DATA_CHARS: { + XML_CharacterDataHandler charDataHandler = parser->m_characterDataHandler; + if (charDataHandler) { + if (MUST_CONVERT(enc, s)) { + for (;;) { + ICHAR *dataPtr = (ICHAR *)parser->m_dataBuf; + const enum XML_Convert_Result convert_res = XmlConvert( + enc, &s, next, &dataPtr, (ICHAR *)parser->m_dataBufEnd); + *eventEndPP = next; + beforeHandler(parser); + charDataHandler(parser->m_handlerArg, parser->m_dataBuf, + (int)(dataPtr - (ICHAR *)parser->m_dataBuf)); + afterHandler(parser); + if ((convert_res == XML_CONVERT_COMPLETED) + || (convert_res == XML_CONVERT_INPUT_INCOMPLETE)) + break; + *eventPP = s; + } + } else { + beforeHandler(parser); + charDataHandler(parser->m_handlerArg, (const XML_Char *)s, + (int)((const XML_Char *)next - (const XML_Char *)s)); + afterHandler(parser); + } + } else if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + } break; + case XML_TOK_INVALID: + *eventPP = next; + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL_CHAR: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_PARTIAL_CHAR; + case XML_TOK_PARTIAL: + case XML_TOK_NONE: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_UNCLOSED_CDATA_SECTION; + default: + /* Every token returned by XmlCdataSectionTok() has its own + * explicit case, so this default case will never be executed. + * We retain it as a safety net and exclude it from the coverage + * statistics. + * + * LCOV_EXCL_START + */ + *eventPP = next; + return XML_ERROR_UNEXPECTED_STATE; + /* LCOV_EXCL_STOP */ + } + + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + *eventPP = next; + *nextPtr = next; + return XML_ERROR_NONE; + case XML_FINISHED: + *eventPP = next; + return XML_ERROR_ABORTED; + case XML_PARSING: + if (parser->m_reenter) { + return XML_ERROR_UNEXPECTED_STATE; // LCOV_EXCL_LINE + } + EXPAT_FALLTHROUGH; + default:; + *eventPP = s = next; + } + } + /* not reached */ +} + +#ifdef XML_DTD + +/* The idea here is to avoid using stack for each IGNORE section when + the whole file is parsed with one call. +*/ +static enum XML_Error +ignoreSectionProcessor(XML_Parser parser, const char *start, const char *end, + const char **endPtr) { + enum XML_Error result + = doIgnoreSection(parser, parser->m_encoding, &start, end, endPtr, + (XML_Bool)! parser->m_parsingStatus.finalBuffer); + if (result != XML_ERROR_NONE) + return result; + if (start) { + parser->m_processor = prologProcessor; + return prologProcessor(parser, start, end, endPtr); + } + return result; +} + +/* startPtr gets set to non-null is the section is closed, and to null + if the section is not yet closed. +*/ +static enum XML_Error +doIgnoreSection(XML_Parser parser, const ENCODING *enc, const char **startPtr, + const char *end, const char **nextPtr, XML_Bool haveMore) { + const char *next = *startPtr; /* in case of XML_TOK_NONE or XML_TOK_PARTIAL */ + int tok; + const char *s = *startPtr; + const char **eventPP; + const char **eventEndPP; + if (enc == parser->m_encoding) { + eventPP = &parser->m_eventPtr; + *eventPP = s; + eventEndPP = &parser->m_eventEndPtr; + } else { + /* It's not entirely clear, but it seems the following two lines + * of code cannot be executed. The only occasions on which 'enc' + * is not 'encoding' are when this function is called + * from the internal entity processing, and IGNORE sections are an + * error in internal entities. + * + * Since it really isn't clear that this is true, we keep the code + * and just remove it from our coverage tests. + * + * LCOV_EXCL_START + */ + eventPP = &(parser->m_openInternalEntities->internalEventPtr); + eventEndPP = &(parser->m_openInternalEntities->internalEventEndPtr); + /* LCOV_EXCL_STOP */ + } + *eventPP = s; + *startPtr = NULL; + tok = XmlIgnoreSectionTok(enc, s, end, &next); +# if XML_GE == 1 + if (! accountingDiffTolerated(parser, tok, s, next, __LINE__, + XML_ACCOUNT_DIRECT)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } +# endif + *eventEndPP = next; + switch (tok) { + case XML_TOK_IGNORE_SECT: + if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + *startPtr = next; + *nextPtr = next; + if (parser->m_parsingStatus.parsing == XML_FINISHED) + return XML_ERROR_ABORTED; + else + return XML_ERROR_NONE; + case XML_TOK_INVALID: + *eventPP = next; + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL_CHAR: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_PARTIAL_CHAR; + case XML_TOK_PARTIAL: + case XML_TOK_NONE: + if (haveMore) { + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_SYNTAX; /* XML_ERROR_UNCLOSED_IGNORE_SECTION */ + default: + /* All of the tokens that XmlIgnoreSectionTok() returns have + * explicit cases to handle them, so this default case is never + * executed. We keep it as a safety net anyway, and remove it + * from our test coverage statistics. + * + * LCOV_EXCL_START + */ + *eventPP = next; + return XML_ERROR_UNEXPECTED_STATE; + /* LCOV_EXCL_STOP */ + } + /* not reached */ +} + +#endif /* XML_DTD */ + +static enum XML_Error +initializeEncoding(XML_Parser parser) { + const char *s; +#ifdef XML_UNICODE + char encodingBuf[128]; + /* See comments about `protocolEncodingName` in parserInit() */ + if (! parser->m_protocolEncodingName) + s = NULL; + else { + int i; + for (i = 0; parser->m_protocolEncodingName[i]; i++) { + if (i == sizeof(encodingBuf) - 1 + || (parser->m_protocolEncodingName[i] & ~0x7f) != 0) { + encodingBuf[0] = '\0'; + break; + } + encodingBuf[i] = (char)parser->m_protocolEncodingName[i]; + } + encodingBuf[i] = '\0'; + s = encodingBuf; + } +#else + s = parser->m_protocolEncodingName; +#endif + if ((parser->m_ns ? XmlInitEncodingNS : XmlInitEncoding)( + &parser->m_initEncoding, &parser->m_encoding, s)) + return XML_ERROR_NONE; + return handleUnknownEncoding(parser, parser->m_protocolEncodingName); +} + +static enum XML_Error +processXmlDecl(XML_Parser parser, int isGeneralTextEntity, const char *s, + const char *next) { + const char *encodingName = NULL; + const XML_Char *storedEncName = NULL; + const ENCODING *newEncoding = NULL; + const char *version = NULL; + const char *versionend = NULL; + const XML_Char *storedversion = NULL; + int standalone = -1; + +#if XML_GE == 1 + if (! accountingDiffTolerated(parser, XML_TOK_XML_DECL, s, next, __LINE__, + XML_ACCOUNT_DIRECT)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } +#endif + + if (! (parser->m_ns ? XmlParseXmlDeclNS : XmlParseXmlDecl)( + isGeneralTextEntity, parser->m_encoding, s, next, &parser->m_eventPtr, + &version, &versionend, &encodingName, &newEncoding, &standalone)) { + if (isGeneralTextEntity) + return XML_ERROR_TEXT_DECL; + else + return XML_ERROR_XML_DECL; + } + if (! isGeneralTextEntity && standalone == 1) { + parser->m_dtd->standalone = XML_TRUE; +#ifdef XML_DTD + if (parser->m_paramEntityParsing + == XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE) + parser->m_paramEntityParsing = XML_PARAM_ENTITY_PARSING_NEVER; +#endif /* XML_DTD */ + } + if (parser->m_xmlDeclHandler) { + if (encodingName != NULL) { + storedEncName = poolStoreString( + &parser->m_temp2Pool, parser->m_encoding, encodingName, + encodingName + XmlNameLength(parser->m_encoding, encodingName)); + if (! storedEncName) + return XML_ERROR_NO_MEMORY; + poolFinish(&parser->m_temp2Pool); + } + if (version) { + storedversion + = poolStoreString(&parser->m_temp2Pool, parser->m_encoding, version, + versionend - parser->m_encoding->minBytesPerChar); + if (! storedversion) + return XML_ERROR_NO_MEMORY; + } + beforeHandler(parser); + parser->m_xmlDeclHandler(parser->m_handlerArg, storedversion, storedEncName, + standalone); + afterHandler(parser); + } else if (parser->m_defaultHandler) + reportDefault(parser, parser->m_encoding, s, next); + if (parser->m_protocolEncodingName == NULL) { + if (newEncoding) { + /* Check that the specified encoding does not conflict with what + * the parser has already deduced. Do we have the same number + * of bytes in the smallest representation of a character? If + * this is UTF-16, is it the same endianness? + */ + if (newEncoding->minBytesPerChar != parser->m_encoding->minBytesPerChar + || (newEncoding->minBytesPerChar == 2 + && newEncoding != parser->m_encoding)) { + parser->m_eventPtr = encodingName; + return XML_ERROR_INCORRECT_ENCODING; + } + parser->m_encoding = newEncoding; + } else if (encodingName) { + enum XML_Error result; + if (! storedEncName) { + storedEncName = poolStoreString( + &parser->m_temp2Pool, parser->m_encoding, encodingName, + encodingName + XmlNameLength(parser->m_encoding, encodingName)); + if (! storedEncName) + return XML_ERROR_NO_MEMORY; + } + result = handleUnknownEncoding(parser, storedEncName); + poolClear(&parser->m_temp2Pool); + if (result == XML_ERROR_UNKNOWN_ENCODING) + parser->m_eventPtr = encodingName; + return result; + } + } + + if (storedEncName || storedversion) + poolClear(&parser->m_temp2Pool); + + return XML_ERROR_NONE; +} + +static enum XML_Error +handleUnknownEncoding(XML_Parser parser, const XML_Char *encodingName) { + if (parser->m_unknownEncodingHandler) { + XML_Encoding info; + int i; + for (i = 0; i < 256; i++) + info.map[i] = -1; + info.convert = NULL; + info.data = NULL; + info.release = NULL; + beforeHandler(parser); + const int status = parser->m_unknownEncodingHandler( + parser->m_unknownEncodingHandlerData, encodingName, &info); + afterHandler(parser); + + parser->m_unknownEncodingRelease = info.release; + parser->m_unknownEncodingData = info.data; + + if (status) { + ENCODING *enc; + parser->m_unknownEncodingMem = MALLOC(parser, XmlSizeOfUnknownEncoding()); + if (! parser->m_unknownEncodingMem) { + if (parser->m_unknownEncodingRelease) + callUnknownEncodingRelease(parser); + else + parser->m_unknownEncodingData = NULL; + return XML_ERROR_NO_MEMORY; + } + parser->m_unknownEncodingConvert = info.convert; + enc = (parser->m_ns ? XmlInitUnknownEncodingNS : XmlInitUnknownEncoding)( + parser->m_unknownEncodingMem, info.map, + info.convert ? callUnknownEncodingConvert : NULL, parser); + if (enc) { + parser->m_encoding = enc; + return XML_ERROR_NONE; + } + parser->m_unknownEncodingConvert = NULL; + } + if (parser->m_unknownEncodingRelease != NULL) + callUnknownEncodingRelease(parser); + else + parser->m_unknownEncodingData = NULL; + } + return XML_ERROR_UNKNOWN_ENCODING; +} + +static enum XML_Error +prologInitProcessor(XML_Parser parser, const char *s, const char *end, + const char **nextPtr) { + enum XML_Error result = initializeEncoding(parser); + if (result != XML_ERROR_NONE) + return result; + parser->m_processor = prologProcessor; + return prologProcessor(parser, s, end, nextPtr); +} + +#ifdef XML_DTD + +static enum XML_Error +externalParEntInitProcessor(XML_Parser parser, const char *s, const char *end, + const char **nextPtr) { + enum XML_Error result = initializeEncoding(parser); + if (result != XML_ERROR_NONE) + return result; + + /* we know now that XML_Parse(Buffer) has been called, + so we consider the external parameter entity read */ + parser->m_dtd->paramEntityRead = true; + + if (parser->m_prologState.inEntityValue) { + parser->m_processor = entityValueInitProcessor; + return entityValueInitProcessor(parser, s, end, nextPtr); + } else { + parser->m_processor = externalParEntProcessor; + return externalParEntProcessor(parser, s, end, nextPtr); + } +} + +static enum XML_Error +entityValueInitProcessor(XML_Parser parser, const char *s, const char *end, + const char **nextPtr) { + int tok; + const char *start = s; + const char *next = start; + parser->m_eventPtr = start; + + for (;;) { + tok = XmlPrologTok(parser->m_encoding, start, end, &next); + /* Note: Except for XML_TOK_BOM below, these bytes are accounted later in: + - storeEntityValue + - processXmlDecl + */ + parser->m_eventEndPtr = next; + if (tok <= 0) { + if (! parser->m_parsingStatus.finalBuffer && tok != XML_TOK_INVALID) { + *nextPtr = s; + return XML_ERROR_NONE; + } + switch (tok) { + case XML_TOK_INVALID: + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL: + return XML_ERROR_UNCLOSED_TOKEN; + case XML_TOK_PARTIAL_CHAR: + return XML_ERROR_PARTIAL_CHAR; + case XML_TOK_NONE: /* start == end */ + default: + break; + } + /* found end of entity value - can store it now */ + return storeEntityValue(parser, parser->m_encoding, s, end, + XML_ACCOUNT_DIRECT, NULL); + } else if (tok == XML_TOK_XML_DECL) { + enum XML_Error result; + result = processXmlDecl(parser, 0, start, next); + if (result != XML_ERROR_NONE) + return result; + /* At this point, m_parsingStatus.parsing cannot be XML_SUSPENDED. For + * that to happen, a parameter entity parsing handler must have attempted + * to suspend the parser, which fails and raises an error. The parser can + * be aborted, but can't be suspended. + */ + if (parser->m_parsingStatus.parsing == XML_FINISHED) + return XML_ERROR_ABORTED; + *nextPtr = next; + /* stop scanning for text declaration - we found one */ + parser->m_processor = entityValueProcessor; + return entityValueProcessor(parser, next, end, nextPtr); + } + /* XmlPrologTok has now set the encoding based on the BOM it found, and we + must move s and nextPtr forward to consume the BOM. + + If we didn't, and got XML_TOK_NONE from the next XmlPrologTok call, we + would leave the BOM in the buffer and return. On the next call to this + function, our XmlPrologTok call would return XML_TOK_INVALID, since it + is not valid to have multiple BOMs. + */ + else if (tok == XML_TOK_BOM) { +# if XML_GE == 1 + if (! accountingDiffTolerated(parser, tok, s, next, __LINE__, + XML_ACCOUNT_DIRECT)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } +# endif + + *nextPtr = next; + s = next; + } + /* If we get this token, we have the start of what might be a + normal tag, but not a declaration (i.e. it doesn't begin with + "m_eventPtr = start; + } +} + +static enum XML_Error +externalParEntProcessor(XML_Parser parser, const char *s, const char *end, + const char **nextPtr) { + const char *next = s; + int tok; + + tok = XmlPrologTok(parser->m_encoding, s, end, &next); + if (tok <= 0) { + if (! parser->m_parsingStatus.finalBuffer && tok != XML_TOK_INVALID) { + *nextPtr = s; + return XML_ERROR_NONE; + } + switch (tok) { + case XML_TOK_INVALID: + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL: + return XML_ERROR_UNCLOSED_TOKEN; + case XML_TOK_PARTIAL_CHAR: + return XML_ERROR_PARTIAL_CHAR; + case XML_TOK_NONE: /* start == end */ + default: + break; + } + } + /* This would cause the next stage, i.e. doProlog to be passed XML_TOK_BOM. + However, when parsing an external subset, doProlog will not accept a BOM + as valid, and report a syntax error, so we have to skip the BOM, and + account for the BOM bytes. + */ + else if (tok == XML_TOK_BOM) { + if (! accountingDiffTolerated(parser, tok, s, next, __LINE__, + XML_ACCOUNT_DIRECT)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } + + s = next; + tok = XmlPrologTok(parser->m_encoding, s, end, &next); + } + + parser->m_processor = prologProcessor; + return doProlog(parser, parser->m_encoding, s, end, tok, next, nextPtr, + (XML_Bool)! parser->m_parsingStatus.finalBuffer, XML_TRUE, + XML_ACCOUNT_DIRECT); +} + +static enum XML_Error +entityValueProcessor(XML_Parser parser, const char *s, const char *end, + const char **nextPtr) { + const char *start = s; + const char *next = s; + const ENCODING *enc = parser->m_encoding; + int tok; + + for (;;) { + tok = XmlPrologTok(enc, start, end, &next); + /* Note: These bytes are accounted later in: + - storeEntityValue + */ + if (tok <= 0) { + if (! parser->m_parsingStatus.finalBuffer && tok != XML_TOK_INVALID) { + *nextPtr = s; + return XML_ERROR_NONE; + } + switch (tok) { + case XML_TOK_INVALID: + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL: + return XML_ERROR_UNCLOSED_TOKEN; + case XML_TOK_PARTIAL_CHAR: + return XML_ERROR_PARTIAL_CHAR; + case XML_TOK_NONE: /* start == end */ + default: + break; + } + /* found end of entity value - can store it now */ + return storeEntityValue(parser, enc, s, end, XML_ACCOUNT_DIRECT, NULL); + } + /* If we get this token, we have the start of what might be a + normal tag, but not a declaration (i.e. it doesn't begin with + "m_encoding, s, end, &next); + return doProlog(parser, parser->m_encoding, s, end, tok, next, nextPtr, + (XML_Bool)! parser->m_parsingStatus.finalBuffer, XML_TRUE, + XML_ACCOUNT_DIRECT); +} + +static enum XML_Error +doProlog(XML_Parser parser, const ENCODING *enc, const char *s, const char *end, + int tok, const char *next, const char **nextPtr, XML_Bool haveMore, + XML_Bool allowClosingDoctype, enum XML_Account account) { +#ifdef XML_DTD + static const XML_Char externalSubsetName[] = {ASCII_HASH, '\0'}; +#endif /* XML_DTD */ + static const XML_Char atypeCDATA[] + = {ASCII_C, ASCII_D, ASCII_A, ASCII_T, ASCII_A, '\0'}; + static const XML_Char atypeID[] = {ASCII_I, ASCII_D, '\0'}; + static const XML_Char atypeIDREF[] + = {ASCII_I, ASCII_D, ASCII_R, ASCII_E, ASCII_F, '\0'}; + static const XML_Char atypeIDREFS[] + = {ASCII_I, ASCII_D, ASCII_R, ASCII_E, ASCII_F, ASCII_S, '\0'}; + static const XML_Char atypeENTITY[] + = {ASCII_E, ASCII_N, ASCII_T, ASCII_I, ASCII_T, ASCII_Y, '\0'}; + static const XML_Char atypeENTITIES[] + = {ASCII_E, ASCII_N, ASCII_T, ASCII_I, ASCII_T, + ASCII_I, ASCII_E, ASCII_S, '\0'}; + static const XML_Char atypeNMTOKEN[] + = {ASCII_N, ASCII_M, ASCII_T, ASCII_O, ASCII_K, ASCII_E, ASCII_N, '\0'}; + static const XML_Char atypeNMTOKENS[] + = {ASCII_N, ASCII_M, ASCII_T, ASCII_O, ASCII_K, + ASCII_E, ASCII_N, ASCII_S, '\0'}; + static const XML_Char notationPrefix[] + = {ASCII_N, ASCII_O, ASCII_T, ASCII_A, ASCII_T, + ASCII_I, ASCII_O, ASCII_N, ASCII_LPAREN, '\0'}; + static const XML_Char enumValueSep[] = {ASCII_PIPE, '\0'}; + static const XML_Char enumValueStart[] = {ASCII_LPAREN, '\0'}; + +#ifndef XML_DTD + UNUSED_P(account); +#endif + + /* save one level of indirection */ + DTD *const dtd = parser->m_dtd; + + const char **eventPP; + const char **eventEndPP; + enum XML_Content_Quant quant; + + if (enc == parser->m_encoding) { + eventPP = &parser->m_eventPtr; + eventEndPP = &parser->m_eventEndPtr; + } else { + eventPP = &(parser->m_openInternalEntities->internalEventPtr); + eventEndPP = &(parser->m_openInternalEntities->internalEventEndPtr); + } + + for (;;) { + int role; + XML_Bool handleDefault = XML_TRUE; + *eventPP = s; + *eventEndPP = next; + if (tok <= 0) { + if (haveMore && tok != XML_TOK_INVALID) { + *nextPtr = s; + return XML_ERROR_NONE; + } + switch (tok) { + case XML_TOK_INVALID: + *eventPP = next; + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL: + return XML_ERROR_UNCLOSED_TOKEN; + case XML_TOK_PARTIAL_CHAR: + return XML_ERROR_PARTIAL_CHAR; + case -XML_TOK_PROLOG_S: + tok = -tok; + break; + case XML_TOK_NONE: +#ifdef XML_DTD + /* for internal PE NOT referenced between declarations */ + if (enc != parser->m_encoding + && ! parser->m_openInternalEntities->betweenDecl) { + *nextPtr = s; + return XML_ERROR_NONE; + } + /* WFC: PE Between Declarations - must check that PE contains + complete markup, not only for external PEs, but also for + internal PEs if the reference occurs between declarations. + */ + if (parser->m_isParamEntity || enc != parser->m_encoding) { + if (XmlTokenRole(&parser->m_prologState, XML_TOK_NONE, end, end, enc) + == XML_ROLE_ERROR) + return XML_ERROR_INCOMPLETE_PE; + *nextPtr = s; + return XML_ERROR_NONE; + } +#endif /* XML_DTD */ + return XML_ERROR_NO_ELEMENTS; + default: + tok = -tok; + next = end; + break; + } + } + role = XmlTokenRole(&parser->m_prologState, tok, s, next, enc); +#if XML_GE == 1 + switch (role) { + case XML_ROLE_INSTANCE_START: // bytes accounted in contentProcessor + case XML_ROLE_XML_DECL: // bytes accounted in processXmlDecl +# ifdef XML_DTD + case XML_ROLE_TEXT_DECL: // bytes accounted in processXmlDecl +# endif + break; + default: + if (! accountingDiffTolerated(parser, tok, s, next, __LINE__, account)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } + } +#endif + switch (role) { + case XML_ROLE_XML_DECL: { + enum XML_Error result = processXmlDecl(parser, 0, s, next); + if (result != XML_ERROR_NONE) + return result; + enc = parser->m_encoding; + handleDefault = XML_FALSE; + } break; + case XML_ROLE_DOCTYPE_NAME: + if (parser->m_startDoctypeDeclHandler) { + parser->m_doctypeName + = poolStoreString(&parser->m_tempPool, enc, s, next); + if (! parser->m_doctypeName) + return XML_ERROR_NO_MEMORY; + poolFinish(&parser->m_tempPool); + parser->m_doctypePubid = NULL; + handleDefault = XML_FALSE; + } + parser->m_doctypeSysid = NULL; /* always initialize to NULL */ + break; + case XML_ROLE_DOCTYPE_INTERNAL_SUBSET: + if (parser->m_startDoctypeDeclHandler) { + beforeHandler(parser); + parser->m_startDoctypeDeclHandler( + parser->m_handlerArg, parser->m_doctypeName, parser->m_doctypeSysid, + parser->m_doctypePubid, 1); + afterHandler(parser); + parser->m_doctypeName = NULL; + poolClear(&parser->m_tempPool); + handleDefault = XML_FALSE; + } + break; +#ifdef XML_DTD + case XML_ROLE_TEXT_DECL: { + enum XML_Error result = processXmlDecl(parser, 1, s, next); + if (result != XML_ERROR_NONE) + return result; + enc = parser->m_encoding; + handleDefault = XML_FALSE; + } break; +#endif /* XML_DTD */ + case XML_ROLE_DOCTYPE_PUBLIC_ID: +#ifdef XML_DTD + parser->m_useForeignDTD = XML_FALSE; + parser->m_declEntity = (ENTITY *)lookup( + parser, &dtd->paramEntities, externalSubsetName, sizeof(ENTITY)); + if (! parser->m_declEntity) + return XML_ERROR_NO_MEMORY; +#endif /* XML_DTD */ + dtd->hasParamEntityRefs = XML_TRUE; + if (parser->m_startDoctypeDeclHandler) { + XML_Char *pubId; + if (! XmlIsPublicId(enc, s, next, eventPP)) + return XML_ERROR_PUBLICID; + pubId = poolStoreString(&parser->m_tempPool, enc, + s + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! pubId) + return XML_ERROR_NO_MEMORY; + normalizePublicId(pubId); + poolFinish(&parser->m_tempPool); + parser->m_doctypePubid = pubId; + handleDefault = XML_FALSE; + goto alreadyChecked; + } + EXPAT_FALLTHROUGH; + case XML_ROLE_ENTITY_PUBLIC_ID: + if (! XmlIsPublicId(enc, s, next, eventPP)) + return XML_ERROR_PUBLICID; + alreadyChecked: + if (dtd->keepProcessing && parser->m_declEntity) { + XML_Char *tem + = poolStoreString(&dtd->pool, enc, s + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! tem) + return XML_ERROR_NO_MEMORY; + normalizePublicId(tem); + parser->m_declEntity->publicId = tem; + poolFinish(&dtd->pool); + /* Don't suppress the default handler if we fell through from + * the XML_ROLE_DOCTYPE_PUBLIC_ID case. + */ + if (parser->m_entityDeclHandler && role == XML_ROLE_ENTITY_PUBLIC_ID) + handleDefault = XML_FALSE; + } + break; + case XML_ROLE_DOCTYPE_CLOSE: + if (allowClosingDoctype != XML_TRUE) { + /* Must not close doctype from within expanded parameter entities */ + return XML_ERROR_INVALID_TOKEN; + } + + if (parser->m_doctypeName) { + beforeHandler(parser); + parser->m_startDoctypeDeclHandler( + parser->m_handlerArg, parser->m_doctypeName, parser->m_doctypeSysid, + parser->m_doctypePubid, 0); + afterHandler(parser); + poolClear(&parser->m_tempPool); + handleDefault = XML_FALSE; + } + /* parser->m_doctypeSysid will be non-NULL in the case of a previous + XML_ROLE_DOCTYPE_SYSTEM_ID, even if parser->m_startDoctypeDeclHandler + was not set, indicating an external subset + */ +#ifdef XML_DTD + if (parser->m_doctypeSysid || parser->m_useForeignDTD) { + XML_Bool hadParamEntityRefs = dtd->hasParamEntityRefs; + dtd->hasParamEntityRefs = XML_TRUE; + if (parser->m_paramEntityParsing + && parser->m_externalEntityRefHandler) { + ENTITY *entity = (ENTITY *)lookup(parser, &dtd->paramEntities, + externalSubsetName, sizeof(ENTITY)); + if (! entity) { + /* The external subset name "#" will have already been + * inserted into the hash table at the start of the + * external entity parsing, so no allocation will happen + * and lookup() cannot fail. + */ + return XML_ERROR_NO_MEMORY; /* LCOV_EXCL_LINE */ + } + if (parser->m_useForeignDTD) + entity->base = parser->m_curBase; + dtd->paramEntityRead = false; + beforeHandler(parser); + const int status = parser->m_externalEntityRefHandler( + parser->m_externalEntityRefHandlerArg, 0, entity->base, + entity->systemId, entity->publicId); + afterHandler(parser); + if (! status) + return XML_ERROR_EXTERNAL_ENTITY_HANDLING; + if (dtd->paramEntityRead) { + if (! dtd->standalone && parser->m_notStandaloneHandler) { + beforeHandler(parser); + const int handlerStatus + = parser->m_notStandaloneHandler(parser->m_handlerArg); + afterHandler(parser); + if (! handlerStatus) + return XML_ERROR_NOT_STANDALONE; + } + } + /* if we didn't read the foreign DTD then this means that there + is no external subset and we must reset dtd->hasParamEntityRefs + */ + else if (! parser->m_doctypeSysid) + dtd->hasParamEntityRefs = hadParamEntityRefs; + /* end of DTD - no need to update dtd->keepProcessing */ + } + parser->m_useForeignDTD = XML_FALSE; + } +#endif /* XML_DTD */ + if (parser->m_endDoctypeDeclHandler) { + beforeHandler(parser); + parser->m_endDoctypeDeclHandler(parser->m_handlerArg); + afterHandler(parser); + handleDefault = XML_FALSE; + } + break; + case XML_ROLE_INSTANCE_START: +#ifdef XML_DTD + /* if there is no DOCTYPE declaration then now is the + last chance to read the foreign DTD + */ + if (parser->m_useForeignDTD) { + XML_Bool hadParamEntityRefs = dtd->hasParamEntityRefs; + dtd->hasParamEntityRefs = XML_TRUE; + if (parser->m_paramEntityParsing + && parser->m_externalEntityRefHandler) { + ENTITY *entity = (ENTITY *)lookup(parser, &dtd->paramEntities, + externalSubsetName, sizeof(ENTITY)); + if (! entity) + return XML_ERROR_NO_MEMORY; + entity->base = parser->m_curBase; + dtd->paramEntityRead = false; + beforeHandler(parser); + const int status = parser->m_externalEntityRefHandler( + parser->m_externalEntityRefHandlerArg, 0, entity->base, + entity->systemId, entity->publicId); + afterHandler(parser); + if (! status) + return XML_ERROR_EXTERNAL_ENTITY_HANDLING; + if (dtd->paramEntityRead) { + if (! dtd->standalone && parser->m_notStandaloneHandler) { + beforeHandler(parser); + const int handlerStatus + = parser->m_notStandaloneHandler(parser->m_handlerArg); + afterHandler(parser); + if (! handlerStatus) + return XML_ERROR_NOT_STANDALONE; + } + } + /* if we didn't read the foreign DTD then this means that there + is no external subset and we must reset dtd->hasParamEntityRefs + */ + else + dtd->hasParamEntityRefs = hadParamEntityRefs; + /* end of DTD - no need to update dtd->keepProcessing */ + } + } +#endif /* XML_DTD */ + parser->m_processor = contentProcessor; + return contentProcessor(parser, s, end, nextPtr); + case XML_ROLE_ATTLIST_ELEMENT_NAME: + parser->m_declElementType = getElementType(parser, enc, s, next); + if (! parser->m_declElementType) + return XML_ERROR_NO_MEMORY; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_NAME: + parser->m_declAttributeId = getAttributeId(parser, enc, s, next); + if (! parser->m_declAttributeId) + return XML_ERROR_NO_MEMORY; + parser->m_declAttributeIsCdata = XML_FALSE; + parser->m_declAttributeType = NULL; + parser->m_declAttributeIsId = XML_FALSE; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_TYPE_CDATA: + parser->m_declAttributeIsCdata = XML_TRUE; + parser->m_declAttributeType = atypeCDATA; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_TYPE_ID: + parser->m_declAttributeIsId = XML_TRUE; + parser->m_declAttributeType = atypeID; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_TYPE_IDREF: + parser->m_declAttributeType = atypeIDREF; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_TYPE_IDREFS: + parser->m_declAttributeType = atypeIDREFS; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_TYPE_ENTITY: + parser->m_declAttributeType = atypeENTITY; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_TYPE_ENTITIES: + parser->m_declAttributeType = atypeENTITIES; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_TYPE_NMTOKEN: + parser->m_declAttributeType = atypeNMTOKEN; + goto checkAttListDeclHandler; + case XML_ROLE_ATTRIBUTE_TYPE_NMTOKENS: + parser->m_declAttributeType = atypeNMTOKENS; + checkAttListDeclHandler: + if (dtd->keepProcessing && parser->m_attlistDeclHandler) + handleDefault = XML_FALSE; + break; + case XML_ROLE_ATTRIBUTE_ENUM_VALUE: + case XML_ROLE_ATTRIBUTE_NOTATION_VALUE: + if (dtd->keepProcessing && parser->m_attlistDeclHandler) { + const XML_Char *prefix; + if (parser->m_declAttributeType) { + prefix = enumValueSep; + } else { + prefix = (role == XML_ROLE_ATTRIBUTE_NOTATION_VALUE ? notationPrefix + : enumValueStart); + } + if (! poolAppendString(&parser->m_tempPool, prefix)) + return XML_ERROR_NO_MEMORY; + if (! poolAppend(&parser->m_tempPool, enc, s, next)) + return XML_ERROR_NO_MEMORY; + parser->m_declAttributeType = parser->m_tempPool.start; + handleDefault = XML_FALSE; + } + break; + case XML_ROLE_IMPLIED_ATTRIBUTE_VALUE: + case XML_ROLE_REQUIRED_ATTRIBUTE_VALUE: + if (dtd->keepProcessing) { + if (! defineAttribute(parser->m_declElementType, + parser->m_declAttributeId, + parser->m_declAttributeIsCdata, + parser->m_declAttributeIsId, 0, parser)) + return XML_ERROR_NO_MEMORY; + if (parser->m_attlistDeclHandler && parser->m_declAttributeType) { + if (*parser->m_declAttributeType == XML_T(ASCII_LPAREN) + || (*parser->m_declAttributeType == XML_T(ASCII_N) + && parser->m_declAttributeType[1] == XML_T(ASCII_O))) { + /* Enumerated or Notation type */ + if (! poolAppendChar(&parser->m_tempPool, XML_T(ASCII_RPAREN)) + || ! poolAppendChar(&parser->m_tempPool, XML_T('\0'))) + return XML_ERROR_NO_MEMORY; + parser->m_declAttributeType = parser->m_tempPool.start; + poolFinish(&parser->m_tempPool); + } + *eventEndPP = s; + beforeHandler(parser); + parser->m_attlistDeclHandler( + parser->m_handlerArg, parser->m_declElementType->name, + parser->m_declAttributeId->name, parser->m_declAttributeType, 0, + role == XML_ROLE_REQUIRED_ATTRIBUTE_VALUE); + afterHandler(parser); + handleDefault = XML_FALSE; + } + } + poolClear(&parser->m_tempPool); + break; + case XML_ROLE_DEFAULT_ATTRIBUTE_VALUE: + case XML_ROLE_FIXED_ATTRIBUTE_VALUE: + if (dtd->keepProcessing) { + const XML_Char *attVal; + enum XML_Error result = storeAttributeValue( + parser, enc, parser->m_declAttributeIsCdata, + s + enc->minBytesPerChar, next - enc->minBytesPerChar, &dtd->pool, + XML_ACCOUNT_NONE); + if (result) + return result; + attVal = poolStart(&dtd->pool); + poolFinish(&dtd->pool); + /* ID attributes aren't allowed to have a default */ + if (! defineAttribute( + parser->m_declElementType, parser->m_declAttributeId, + parser->m_declAttributeIsCdata, XML_FALSE, attVal, parser)) + return XML_ERROR_NO_MEMORY; + if (parser->m_attlistDeclHandler && parser->m_declAttributeType) { + if (*parser->m_declAttributeType == XML_T(ASCII_LPAREN) + || (*parser->m_declAttributeType == XML_T(ASCII_N) + && parser->m_declAttributeType[1] == XML_T(ASCII_O))) { + /* Enumerated or Notation type */ + if (! poolAppendChar(&parser->m_tempPool, XML_T(ASCII_RPAREN)) + || ! poolAppendChar(&parser->m_tempPool, XML_T('\0'))) + return XML_ERROR_NO_MEMORY; + parser->m_declAttributeType = parser->m_tempPool.start; + poolFinish(&parser->m_tempPool); + } + *eventEndPP = s; + beforeHandler(parser); + parser->m_attlistDeclHandler( + parser->m_handlerArg, parser->m_declElementType->name, + parser->m_declAttributeId->name, parser->m_declAttributeType, + attVal, role == XML_ROLE_FIXED_ATTRIBUTE_VALUE); + afterHandler(parser); + poolClear(&parser->m_tempPool); + handleDefault = XML_FALSE; + } + } + break; + case XML_ROLE_ENTITY_VALUE: + if (dtd->keepProcessing) { +#if XML_GE == 1 + // This will store the given replacement text in + // parser->m_declEntity->textPtr. + enum XML_Error result = callStoreEntityValue( + parser, enc, s + enc->minBytesPerChar, next - enc->minBytesPerChar, + XML_ACCOUNT_NONE); + if (parser->m_declEntity) { + /* Detect and prevent signed integer overflow */ + if ((size_t)poolLength(&dtd->entityValuePool) > (size_t)INT_MAX) { + return XML_ERROR_NO_MEMORY; + } + parser->m_declEntity->textPtr = poolStart(&dtd->entityValuePool); + parser->m_declEntity->textLen + = (int)(poolLength(&dtd->entityValuePool)); + poolFinish(&dtd->entityValuePool); + if (parser->m_entityDeclHandler) { + *eventEndPP = s; + beforeHandler(parser); + parser->m_entityDeclHandler( + parser->m_handlerArg, parser->m_declEntity->name, + parser->m_declEntity->is_param, parser->m_declEntity->textPtr, + parser->m_declEntity->textLen, parser->m_curBase, 0, 0, 0); + afterHandler(parser); + handleDefault = XML_FALSE; + } + } else + poolDiscard(&dtd->entityValuePool); + if (result != XML_ERROR_NONE) + return result; +#else + // This will store "&entity123;" in parser->m_declEntity->textPtr + // to end up as "&entity123;" in the handler. + if (parser->m_declEntity != NULL) { + const enum XML_Error result + = storeSelfEntityValue(parser, parser->m_declEntity); + if (result != XML_ERROR_NONE) + return result; + + if (parser->m_entityDeclHandler) { + *eventEndPP = s; + beforeHandler(parser); + parser->m_entityDeclHandler( + parser->m_handlerArg, parser->m_declEntity->name, + parser->m_declEntity->is_param, parser->m_declEntity->textPtr, + parser->m_declEntity->textLen, parser->m_curBase, 0, 0, 0); + afterHandler(parser); + handleDefault = XML_FALSE; + } + } +#endif + } + break; + case XML_ROLE_DOCTYPE_SYSTEM_ID: +#ifdef XML_DTD + parser->m_useForeignDTD = XML_FALSE; +#endif /* XML_DTD */ + dtd->hasParamEntityRefs = XML_TRUE; + if (parser->m_startDoctypeDeclHandler) { + parser->m_doctypeSysid = poolStoreString(&parser->m_tempPool, enc, + s + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (parser->m_doctypeSysid == NULL) + return XML_ERROR_NO_MEMORY; + poolFinish(&parser->m_tempPool); + handleDefault = XML_FALSE; + } +#ifdef XML_DTD + else + /* use externalSubsetName to make parser->m_doctypeSysid non-NULL + for the case where no parser->m_startDoctypeDeclHandler is set */ + parser->m_doctypeSysid = externalSubsetName; +#endif /* XML_DTD */ + if (! dtd->standalone +#ifdef XML_DTD + && ! parser->m_paramEntityParsing +#endif /* XML_DTD */ + && parser->m_notStandaloneHandler) { + beforeHandler(parser); + const int status = parser->m_notStandaloneHandler(parser->m_handlerArg); + afterHandler(parser); + if (! status) + return XML_ERROR_NOT_STANDALONE; + } +#ifndef XML_DTD + break; +#else /* XML_DTD */ + if (! parser->m_declEntity) { + parser->m_declEntity = (ENTITY *)lookup( + parser, &dtd->paramEntities, externalSubsetName, sizeof(ENTITY)); + if (! parser->m_declEntity) + return XML_ERROR_NO_MEMORY; + parser->m_declEntity->publicId = NULL; + } +#endif /* XML_DTD */ + EXPAT_FALLTHROUGH; + case XML_ROLE_ENTITY_SYSTEM_ID: + if (dtd->keepProcessing && parser->m_declEntity) { + parser->m_declEntity->systemId + = poolStoreString(&dtd->pool, enc, s + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! parser->m_declEntity->systemId) + return XML_ERROR_NO_MEMORY; + parser->m_declEntity->base = parser->m_curBase; + poolFinish(&dtd->pool); + /* Don't suppress the default handler if we fell through from + * the XML_ROLE_DOCTYPE_SYSTEM_ID case. + */ + if (parser->m_entityDeclHandler && role == XML_ROLE_ENTITY_SYSTEM_ID) + handleDefault = XML_FALSE; + } + break; + case XML_ROLE_ENTITY_COMPLETE: +#if XML_GE == 0 + // This will store "&entity123;" in entity->textPtr + // to end up as "&entity123;" in the handler. + if (parser->m_declEntity != NULL) { + const enum XML_Error result + = storeSelfEntityValue(parser, parser->m_declEntity); + if (result != XML_ERROR_NONE) + return result; + } +#endif + if (dtd->keepProcessing && parser->m_declEntity + && parser->m_entityDeclHandler) { + *eventEndPP = s; + beforeHandler(parser); + parser->m_entityDeclHandler( + parser->m_handlerArg, parser->m_declEntity->name, + parser->m_declEntity->is_param, 0, 0, parser->m_declEntity->base, + parser->m_declEntity->systemId, parser->m_declEntity->publicId, 0); + afterHandler(parser); + handleDefault = XML_FALSE; + } + break; + case XML_ROLE_ENTITY_NOTATION_NAME: + if (dtd->keepProcessing && parser->m_declEntity) { + parser->m_declEntity->notation + = poolStoreString(&dtd->pool, enc, s, next); + if (! parser->m_declEntity->notation) + return XML_ERROR_NO_MEMORY; + poolFinish(&dtd->pool); + if (parser->m_unparsedEntityDeclHandler) { + *eventEndPP = s; + beforeHandler(parser); + parser->m_unparsedEntityDeclHandler( + parser->m_handlerArg, parser->m_declEntity->name, + parser->m_declEntity->base, parser->m_declEntity->systemId, + parser->m_declEntity->publicId, parser->m_declEntity->notation); + afterHandler(parser); + handleDefault = XML_FALSE; + } else if (parser->m_entityDeclHandler) { + *eventEndPP = s; + beforeHandler(parser); + parser->m_entityDeclHandler( + parser->m_handlerArg, parser->m_declEntity->name, 0, 0, 0, + parser->m_declEntity->base, parser->m_declEntity->systemId, + parser->m_declEntity->publicId, parser->m_declEntity->notation); + afterHandler(parser); + handleDefault = XML_FALSE; + } + } + break; + case XML_ROLE_GENERAL_ENTITY_NAME: { + if (XmlPredefinedEntityName(enc, s, next)) { + parser->m_declEntity = NULL; + break; + } + if (dtd->keepProcessing) { + const XML_Char *name = poolStoreString(&dtd->pool, enc, s, next); + if (! name) + return XML_ERROR_NO_MEMORY; + parser->m_declEntity = (ENTITY *)lookup(parser, &dtd->generalEntities, + name, sizeof(ENTITY)); + if (! parser->m_declEntity) + return XML_ERROR_NO_MEMORY; + if (parser->m_declEntity->name != name) { + poolDiscard(&dtd->pool); + parser->m_declEntity = NULL; + } else { + poolFinish(&dtd->pool); + parser->m_declEntity->publicId = NULL; + parser->m_declEntity->is_param = false; + /* if we have a parent parser or are reading an internal parameter + entity, then the entity declaration is not considered "internal" + */ + parser->m_declEntity->is_internal + = ! (parser->m_parentParser || parser->m_openInternalEntities); + if (parser->m_entityDeclHandler) + handleDefault = XML_FALSE; + } + } else { + poolDiscard(&dtd->pool); + parser->m_declEntity = NULL; + } + } break; + case XML_ROLE_PARAM_ENTITY_NAME: +#ifdef XML_DTD + if (dtd->keepProcessing) { + const XML_Char *name = poolStoreString(&dtd->pool, enc, s, next); + if (! name) + return XML_ERROR_NO_MEMORY; + parser->m_declEntity = (ENTITY *)lookup(parser, &dtd->paramEntities, + name, sizeof(ENTITY)); + if (! parser->m_declEntity) + return XML_ERROR_NO_MEMORY; + if (parser->m_declEntity->name != name) { + poolDiscard(&dtd->pool); + parser->m_declEntity = NULL; + } else { + poolFinish(&dtd->pool); + parser->m_declEntity->publicId = NULL; + parser->m_declEntity->is_param = true; + /* if we have a parent parser or are reading an internal parameter + entity, then the entity declaration is not considered "internal" + */ + parser->m_declEntity->is_internal + = ! (parser->m_parentParser || parser->m_openInternalEntities); + if (parser->m_entityDeclHandler) + handleDefault = XML_FALSE; + } + } else { + poolDiscard(&dtd->pool); + parser->m_declEntity = NULL; + } +#else /* not XML_DTD */ + parser->m_declEntity = NULL; +#endif /* XML_DTD */ + break; + case XML_ROLE_NOTATION_NAME: + parser->m_declNotationPublicId = NULL; + parser->m_declNotationName = NULL; + if (parser->m_notationDeclHandler) { + parser->m_declNotationName + = poolStoreString(&parser->m_tempPool, enc, s, next); + if (! parser->m_declNotationName) + return XML_ERROR_NO_MEMORY; + poolFinish(&parser->m_tempPool); + handleDefault = XML_FALSE; + } + break; + case XML_ROLE_NOTATION_PUBLIC_ID: + if (! XmlIsPublicId(enc, s, next, eventPP)) + return XML_ERROR_PUBLICID; + if (parser + ->m_declNotationName) { /* means m_notationDeclHandler != NULL */ + XML_Char *tem = poolStoreString(&parser->m_tempPool, enc, + s + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! tem) + return XML_ERROR_NO_MEMORY; + normalizePublicId(tem); + parser->m_declNotationPublicId = tem; + poolFinish(&parser->m_tempPool); + handleDefault = XML_FALSE; + } + break; + case XML_ROLE_NOTATION_SYSTEM_ID: + if (parser->m_declNotationName && parser->m_notationDeclHandler) { + const XML_Char *systemId = poolStoreString(&parser->m_tempPool, enc, + s + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! systemId) + return XML_ERROR_NO_MEMORY; + *eventEndPP = s; + beforeHandler(parser); + parser->m_notationDeclHandler( + parser->m_handlerArg, parser->m_declNotationName, parser->m_curBase, + systemId, parser->m_declNotationPublicId); + afterHandler(parser); + handleDefault = XML_FALSE; + } + poolClear(&parser->m_tempPool); + break; + case XML_ROLE_NOTATION_NO_SYSTEM_ID: + if (parser->m_declNotationPublicId && parser->m_notationDeclHandler) { + *eventEndPP = s; + beforeHandler(parser); + parser->m_notationDeclHandler( + parser->m_handlerArg, parser->m_declNotationName, parser->m_curBase, + 0, parser->m_declNotationPublicId); + afterHandler(parser); + handleDefault = XML_FALSE; + } + poolClear(&parser->m_tempPool); + break; + case XML_ROLE_ERROR: + switch (tok) { + case XML_TOK_PARAM_ENTITY_REF: + /* PE references in internal subset are + not allowed within declarations. */ + return XML_ERROR_PARAM_ENTITY_REF; + case XML_TOK_XML_DECL: + return XML_ERROR_MISPLACED_XML_PI; + default: + return XML_ERROR_SYNTAX; + } +#ifdef XML_DTD + case XML_ROLE_IGNORE_SECT: { + enum XML_Error result; + if (parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + handleDefault = XML_FALSE; + result = doIgnoreSection(parser, enc, &next, end, nextPtr, haveMore); + if (result != XML_ERROR_NONE) + return result; + else if (! next) { + parser->m_processor = ignoreSectionProcessor; + return result; + } + } break; +#endif /* XML_DTD */ + case XML_ROLE_GROUP_OPEN: + if (parser->m_prologState.level >= parser->m_groupSize) { + if (parser->m_groupSize) { + /* Detect and prevent integer overflow */ + if (parser->m_groupSize > SIZE_MAX / 2) { + return XML_ERROR_NO_MEMORY; + } + + char *const new_connector = REALLOC(parser, parser->m_groupConnector, + parser->m_groupSize *= 2); + if (new_connector == NULL) { + parser->m_groupSize /= 2; + return XML_ERROR_NO_MEMORY; + } + parser->m_groupConnector = new_connector; + } else { + parser->m_groupConnector = MALLOC(parser, parser->m_groupSize = 32); + if (! parser->m_groupConnector) { + parser->m_groupSize = 0; + return XML_ERROR_NO_MEMORY; + } + } + } + parser->m_groupConnector[parser->m_prologState.level] = 0; + if (dtd->in_eldecl) { + int myindex = nextScaffoldPart(parser); + if (myindex < 0) + return XML_ERROR_NO_MEMORY; + assert(dtd->scaffIndex != NULL); + if ((size_t)dtd->scaffLevel >= dtd->scaffIndexSize) { + /* Detect and prevent integer overflow */ + if (dtd->scaffIndexSize > SIZE_MAX / 2 / sizeof(int)) { + return XML_ERROR_NO_MEMORY; + } + assert(dtd->scaffIndexSize > 0); + const size_t new_size = dtd->scaffIndexSize * 2; + int *const new_scaff_index + = REALLOC(parser, dtd->scaffIndex, new_size * sizeof(int)); + if (new_scaff_index == NULL) { + return XML_ERROR_NO_MEMORY; + } + dtd->scaffIndex = new_scaff_index; + dtd->scaffIndexSize = new_size; + } + dtd->scaffIndex[dtd->scaffLevel] = myindex; + dtd->scaffLevel++; + dtd->scaffold[myindex].type = XML_CTYPE_SEQ; + if (parser->m_elementDeclHandler) + handleDefault = XML_FALSE; + } + break; + case XML_ROLE_GROUP_SEQUENCE: + if (parser->m_groupConnector[parser->m_prologState.level] == ASCII_PIPE) + return XML_ERROR_SYNTAX; + parser->m_groupConnector[parser->m_prologState.level] = ASCII_COMMA; + if (dtd->in_eldecl && parser->m_elementDeclHandler) + handleDefault = XML_FALSE; + break; + case XML_ROLE_GROUP_CHOICE: + if (parser->m_groupConnector[parser->m_prologState.level] == ASCII_COMMA) + return XML_ERROR_SYNTAX; + if (dtd->in_eldecl + && ! parser->m_groupConnector[parser->m_prologState.level] + && (dtd->scaffold[dtd->scaffIndex[dtd->scaffLevel - 1]].type + != XML_CTYPE_MIXED)) { + dtd->scaffold[dtd->scaffIndex[dtd->scaffLevel - 1]].type + = XML_CTYPE_CHOICE; + if (parser->m_elementDeclHandler) + handleDefault = XML_FALSE; + } + parser->m_groupConnector[parser->m_prologState.level] = ASCII_PIPE; + break; + case XML_ROLE_PARAM_ENTITY_REF: +#ifdef XML_DTD + case XML_ROLE_INNER_PARAM_ENTITY_REF: + dtd->hasParamEntityRefs = XML_TRUE; + if (! parser->m_paramEntityParsing) + dtd->keepProcessing = dtd->standalone; + else { + const XML_Char *name; + ENTITY *entity; + name = poolStoreString(&dtd->pool, enc, s + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! name) + return XML_ERROR_NO_MEMORY; + entity = (ENTITY *)lookup(parser, &dtd->paramEntities, name, 0); + poolDiscard(&dtd->pool); + /* first, determine if a check for an existing declaration is needed; + if yes, check that the entity exists, and that it is internal, + otherwise call the skipped entity handler + */ + if (parser->m_prologState.documentEntity + && (dtd->standalone ? ! parser->m_openInternalEntities + : ! dtd->hasParamEntityRefs)) { + if (! entity) + return XML_ERROR_UNDEFINED_ENTITY; + else if (! entity->is_internal) { + /* It's hard to exhaustively search the code to be sure, + * but there doesn't seem to be a way of executing the + * following line. There are two cases: + * + * If 'standalone' is false, the DTD must have no + * parameter entities or we wouldn't have passed the outer + * 'if' statement. That means the only entity in the hash + * table is the external subset name "#" which cannot be + * given as a parameter entity name in XML syntax, so the + * lookup must have returned NULL and we don't even reach + * the test for an internal entity. + * + * If 'standalone' is true, it does not seem to be + * possible to create entities taking this code path that + * are not internal entities, so fail the test above. + * + * Because this analysis is very uncertain, the code is + * being left in place and merely removed from the + * coverage test statistics. + */ + return XML_ERROR_ENTITY_DECLARED_IN_PE; /* LCOV_EXCL_LINE */ + } + } else if (! entity) { + dtd->keepProcessing = dtd->standalone; + /* cannot report skipped entities in declarations */ + if ((role == XML_ROLE_PARAM_ENTITY_REF) + && parser->m_skippedEntityHandler) { + beforeHandler(parser); + parser->m_skippedEntityHandler(parser->m_handlerArg, name, 1); + afterHandler(parser); + handleDefault = XML_FALSE; + } + break; + } + if (entity->open) + return XML_ERROR_RECURSIVE_ENTITY_REF; + if (entity->textPtr) { + enum XML_Error result; + bool betweenDecl = (role == XML_ROLE_PARAM_ENTITY_REF); + result = processEntity(parser, entity, betweenDecl, ENTITY_INTERNAL); + if (result != XML_ERROR_NONE) + return result; + handleDefault = XML_FALSE; + break; + } + if (parser->m_externalEntityRefHandler) { + dtd->paramEntityRead = false; + entity->open = true; + entityTrackingOnOpen(parser, entity, __LINE__); + beforeHandler(parser); + const int status = parser->m_externalEntityRefHandler( + parser->m_externalEntityRefHandlerArg, 0, entity->base, + entity->systemId, entity->publicId); + afterHandler(parser); + if (! status) { + entityTrackingOnClose(parser, entity, __LINE__); + entity->open = false; + return XML_ERROR_EXTERNAL_ENTITY_HANDLING; + } + entityTrackingOnClose(parser, entity, __LINE__); + entity->open = false; + handleDefault = XML_FALSE; + if (! dtd->paramEntityRead) { + dtd->keepProcessing = dtd->standalone; + break; + } + } else { + dtd->keepProcessing = dtd->standalone; + break; + } + } +#endif /* XML_DTD */ + if (! dtd->standalone && parser->m_notStandaloneHandler) { + beforeHandler(parser); + const int status = parser->m_notStandaloneHandler(parser->m_handlerArg); + afterHandler(parser); + if (! status) + return XML_ERROR_NOT_STANDALONE; + } + break; + + /* Element declaration stuff */ + + case XML_ROLE_ELEMENT_NAME: + if (parser->m_elementDeclHandler) { + parser->m_declElementType = getElementType(parser, enc, s, next); + if (! parser->m_declElementType) + return XML_ERROR_NO_MEMORY; + dtd->scaffLevel = 0; + dtd->scaffCount = 0; + dtd->in_eldecl = true; + handleDefault = XML_FALSE; + } + break; + + case XML_ROLE_CONTENT_ANY: + case XML_ROLE_CONTENT_EMPTY: + if (dtd->in_eldecl) { + if (parser->m_elementDeclHandler) { + // NOTE: We are avoiding MALLOC(..) here to so that + // applications that are not using XML_FreeContentModel but + // plain free(..) or .free_fcn() to free the content model's + // memory are safe. + XML_Content *content = parser->m_mem.malloc_fcn(sizeof(XML_Content)); + if (! content) + return XML_ERROR_NO_MEMORY; + content->quant = XML_CQUANT_NONE; + content->name = NULL; + content->numchildren = 0; + content->children = NULL; + content->type = ((role == XML_ROLE_CONTENT_ANY) ? XML_CTYPE_ANY + : XML_CTYPE_EMPTY); + *eventEndPP = s; + beforeHandler(parser); + parser->m_elementDeclHandler( + parser->m_handlerArg, parser->m_declElementType->name, content); + afterHandler(parser); + handleDefault = XML_FALSE; + } + dtd->in_eldecl = false; + } + break; + + case XML_ROLE_CONTENT_PCDATA: + if (dtd->in_eldecl) { + dtd->scaffold[dtd->scaffIndex[dtd->scaffLevel - 1]].type + = XML_CTYPE_MIXED; + if (parser->m_elementDeclHandler) + handleDefault = XML_FALSE; + } + break; + + case XML_ROLE_CONTENT_ELEMENT: + quant = XML_CQUANT_NONE; + goto elementContent; + case XML_ROLE_CONTENT_ELEMENT_OPT: + quant = XML_CQUANT_OPT; + goto elementContent; + case XML_ROLE_CONTENT_ELEMENT_REP: + quant = XML_CQUANT_REP; + goto elementContent; + case XML_ROLE_CONTENT_ELEMENT_PLUS: + quant = XML_CQUANT_PLUS; + elementContent: + if (dtd->in_eldecl) { + ELEMENT_TYPE *el; + const XML_Char *name; + size_t nameLen; + const char *nxt + = (quant == XML_CQUANT_NONE ? next : next - enc->minBytesPerChar); + int myindex = nextScaffoldPart(parser); + if (myindex < 0) + return XML_ERROR_NO_MEMORY; + dtd->scaffold[myindex].type = XML_CTYPE_NAME; + dtd->scaffold[myindex].quant = quant; + el = getElementType(parser, enc, s, nxt); + if (! el) + return XML_ERROR_NO_MEMORY; + name = el->name; + dtd->scaffold[myindex].name = name; + nameLen = xcslen(name) + /*null terminator*/ 1; + + /* Detect and prevent integer overflow */ + if (nameLen > UINT_MAX - dtd->contentStringLen) { + return XML_ERROR_NO_MEMORY; + } + + dtd->contentStringLen += (unsigned)nameLen; + if (parser->m_elementDeclHandler) + handleDefault = XML_FALSE; + } + break; + + case XML_ROLE_GROUP_CLOSE: + quant = XML_CQUANT_NONE; + goto closeGroup; + case XML_ROLE_GROUP_CLOSE_OPT: + quant = XML_CQUANT_OPT; + goto closeGroup; + case XML_ROLE_GROUP_CLOSE_REP: + quant = XML_CQUANT_REP; + goto closeGroup; + case XML_ROLE_GROUP_CLOSE_PLUS: + quant = XML_CQUANT_PLUS; + closeGroup: + if (dtd->in_eldecl) { + if (parser->m_elementDeclHandler) + handleDefault = XML_FALSE; + dtd->scaffLevel--; + dtd->scaffold[dtd->scaffIndex[dtd->scaffLevel]].quant = quant; + if (dtd->scaffLevel == 0) { + if (! handleDefault) { + XML_Content *model = build_model(parser); + if (! model) + return XML_ERROR_NO_MEMORY; + *eventEndPP = s; + beforeHandler(parser); + parser->m_elementDeclHandler( + parser->m_handlerArg, parser->m_declElementType->name, model); + afterHandler(parser); + } + dtd->in_eldecl = false; + dtd->contentStringLen = 0; + } + } + break; + /* End element declaration stuff */ + + case XML_ROLE_PI: + if (! reportProcessingInstruction(parser, enc, s, next)) + return XML_ERROR_NO_MEMORY; + handleDefault = XML_FALSE; + break; + case XML_ROLE_COMMENT: + if (! reportComment(parser, enc, s, next)) + return XML_ERROR_NO_MEMORY; + handleDefault = XML_FALSE; + break; + case XML_ROLE_NONE: + switch (tok) { + case XML_TOK_BOM: + handleDefault = XML_FALSE; + break; + } + break; + case XML_ROLE_DOCTYPE_NONE: + if (parser->m_startDoctypeDeclHandler) + handleDefault = XML_FALSE; + break; + case XML_ROLE_ENTITY_NONE: + if (dtd->keepProcessing && parser->m_entityDeclHandler) + handleDefault = XML_FALSE; + break; + case XML_ROLE_NOTATION_NONE: + if (parser->m_notationDeclHandler) + handleDefault = XML_FALSE; + break; + case XML_ROLE_ATTLIST_NONE: + if (dtd->keepProcessing && parser->m_attlistDeclHandler) + handleDefault = XML_FALSE; + break; + case XML_ROLE_ELEMENT_NONE: + if (parser->m_elementDeclHandler) + handleDefault = XML_FALSE; + break; + } /* end of big switch */ + + if (handleDefault && parser->m_defaultHandler) + reportDefault(parser, enc, s, next); + + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + *nextPtr = next; + return XML_ERROR_NONE; + case XML_FINISHED: + return XML_ERROR_ABORTED; + case XML_PARSING: + if (parser->m_reenter) { + *nextPtr = next; + return XML_ERROR_NONE; + } + EXPAT_FALLTHROUGH; + default: + s = next; + tok = XmlPrologTok(enc, s, end, &next); + } + } + /* not reached */ +} + +static enum XML_Error +epilogProcessor(XML_Parser parser, const char *s, const char *end, + const char **nextPtr) { + parser->m_processor = epilogProcessor; + parser->m_eventPtr = s; + for (;;) { + const char *next = NULL; + int tok = XmlPrologTok(parser->m_encoding, s, end, &next); +#if XML_GE == 1 + if (! accountingDiffTolerated(parser, tok, s, next, __LINE__, + XML_ACCOUNT_DIRECT)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } +#endif + parser->m_eventEndPtr = next; + switch (tok) { + /* report partial linebreak - it might be the last token */ + case -XML_TOK_PROLOG_S: + if (parser->m_defaultHandler) { + reportDefault(parser, parser->m_encoding, s, next); + if (parser->m_parsingStatus.parsing == XML_FINISHED) + return XML_ERROR_ABORTED; + } + *nextPtr = next; + return XML_ERROR_NONE; + case XML_TOK_NONE: + *nextPtr = s; + return XML_ERROR_NONE; + case XML_TOK_PROLOG_S: + if (parser->m_defaultHandler) + reportDefault(parser, parser->m_encoding, s, next); + break; + case XML_TOK_PI: + if (! reportProcessingInstruction(parser, parser->m_encoding, s, next)) + return XML_ERROR_NO_MEMORY; + break; + case XML_TOK_COMMENT: + if (! reportComment(parser, parser->m_encoding, s, next)) + return XML_ERROR_NO_MEMORY; + break; + case XML_TOK_INVALID: + parser->m_eventPtr = next; + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL: + if (! parser->m_parsingStatus.finalBuffer) { + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_UNCLOSED_TOKEN; + case XML_TOK_PARTIAL_CHAR: + if (! parser->m_parsingStatus.finalBuffer) { + *nextPtr = s; + return XML_ERROR_NONE; + } + return XML_ERROR_PARTIAL_CHAR; + default: + return XML_ERROR_JUNK_AFTER_DOC_ELEMENT; + } + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + parser->m_eventPtr = next; + *nextPtr = next; + return XML_ERROR_NONE; + case XML_FINISHED: + parser->m_eventPtr = next; + return XML_ERROR_ABORTED; + case XML_PARSING: + if (parser->m_reenter) { + return XML_ERROR_UNEXPECTED_STATE; // LCOV_EXCL_LINE + } + EXPAT_FALLTHROUGH; + default:; + parser->m_eventPtr = s = next; + } + } +} + +static enum XML_Error +processEntity(XML_Parser parser, ENTITY *entity, bool betweenDecl, + enum EntityType type) { + OPEN_INTERNAL_ENTITY *openEntity, **openEntityList; + OPEN_INTERNAL_ENTITY **const freeEntityList = &parser->m_freeEntities; + switch (type) { + case ENTITY_INTERNAL: + parser->m_processor = internalEntityProcessor; + openEntityList = &parser->m_openInternalEntities; + break; + case ENTITY_ATTRIBUTE: + openEntityList = &parser->m_openAttributeEntities; + break; + case ENTITY_VALUE: + openEntityList = &parser->m_openValueEntities; + break; + /* default case serves merely as a safety net in case of a + * wrong entityType. Therefore we exclude the following lines + * from the test coverage. + * + * LCOV_EXCL_START + */ + default: + // Should not reach here + assert(0); + /* LCOV_EXCL_STOP */ + } + + if (*freeEntityList) { + openEntity = *freeEntityList; + *freeEntityList = openEntity->next; + } else { + openEntity = MALLOC(parser, sizeof(OPEN_INTERNAL_ENTITY)); + if (! openEntity) + return XML_ERROR_NO_MEMORY; + } + entity->open = true; + entity->hasMore = true; +#if XML_GE == 1 + entityTrackingOnOpen(parser, entity, __LINE__); +#endif + entity->processed = 0; + openEntity->next = *openEntityList; + *openEntityList = openEntity; + openEntity->entity = entity; + openEntity->type = type; + openEntity->startTagLevel = parser->m_tagLevel; + openEntity->betweenDecl = betweenDecl; + openEntity->internalEventPtr = NULL; + openEntity->internalEventEndPtr = NULL; + + // Only internal entities make use of the reenter flag + // therefore no need to set it for other entity types + if (type == ENTITY_INTERNAL) { + triggerReenter(parser); + } + return XML_ERROR_NONE; +} + +static enum XML_Error +internalEntityProcessor(XML_Parser parser, const char *s, const char *end, + const char **nextPtr) { + UNUSED_P(s); + UNUSED_P(end); + UNUSED_P(nextPtr); + ENTITY *entity; + const char *textStart, *textEnd; + const char *next; + enum XML_Error result; + OPEN_INTERNAL_ENTITY *openEntity = parser->m_openInternalEntities; + if (! openEntity) + return XML_ERROR_UNEXPECTED_STATE; + + entity = openEntity->entity; + + // This will return early + if (entity->hasMore) { + textStart = ((const char *)entity->textPtr) + entity->processed; + textEnd = (const char *)(entity->textPtr + entity->textLen); + /* Set a safe default value in case 'next' does not get set */ + next = textStart; + + if (entity->is_param) { + int tok + = XmlPrologTok(parser->m_internalEncoding, textStart, textEnd, &next); + result = doProlog(parser, parser->m_internalEncoding, textStart, textEnd, + tok, next, &next, XML_FALSE, XML_FALSE, + XML_ACCOUNT_ENTITY_EXPANSION); + } else { + result = doContent(parser, openEntity->startTagLevel, + parser->m_internalEncoding, textStart, textEnd, &next, + XML_FALSE, XML_ACCOUNT_ENTITY_EXPANSION); + } + + if (result != XML_ERROR_NONE) + return result; + // Check if entity is complete, if not, mark down how much of it is + // processed + if (textEnd != next + && (parser->m_parsingStatus.parsing == XML_SUSPENDED + || (parser->m_parsingStatus.parsing == XML_PARSING + && parser->m_reenter))) { + entity->processed = (int)(next - (const char *)entity->textPtr); + return result; + } + + // Entity is complete. We cannot close it here since we need to first + // process its possible inner entities (which are added to the + // m_openInternalEntities during doProlog or doContent calls above) + entity->hasMore = false; + if (! entity->is_param + && (openEntity->startTagLevel != parser->m_tagLevel)) { + return XML_ERROR_ASYNC_ENTITY; + } + triggerReenter(parser); + return result; + } // End of entity processing, "if" block will return here + + // Remove fully processed openEntity from open entity list. +#if XML_GE == 1 + entityTrackingOnClose(parser, entity, __LINE__); +#endif + // openEntity is m_openInternalEntities' head, as we set it at the start of + // this function and we skipped doProlog and doContent calls with hasMore set + // to false. This means we can directly remove the head of + // m_openInternalEntities + assert(parser->m_openInternalEntities == openEntity); + entity->open = false; + parser->m_openInternalEntities = parser->m_openInternalEntities->next; + + /* put openEntity back in list of free instances */ + openEntity->next = parser->m_freeEntities; + parser->m_freeEntities = openEntity; + + if (parser->m_openInternalEntities == NULL) { + parser->m_processor = entity->is_param ? prologProcessor : contentProcessor; + } + triggerReenter(parser); + return XML_ERROR_NONE; +} + +static enum XML_Error +errorProcessor(XML_Parser parser, const char *s, const char *end, + const char **nextPtr) { + UNUSED_P(s); + UNUSED_P(end); + UNUSED_P(nextPtr); + return parser->m_errorCode; +} + +static enum XML_Error +storeAttributeValue(XML_Parser parser, const ENCODING *enc, XML_Bool isCdata, + const char *ptr, const char *end, STRING_POOL *pool, + enum XML_Account account) { + const char *next = ptr; + enum XML_Error result = XML_ERROR_NONE; + + while (1) { + if (! parser->m_openAttributeEntities) { + result = appendAttributeValue(parser, enc, isCdata, next, end, pool, + account, &next); + } else { + OPEN_INTERNAL_ENTITY *const openEntity = parser->m_openAttributeEntities; + if (! openEntity) + return XML_ERROR_UNEXPECTED_STATE; + + ENTITY *const entity = openEntity->entity; + const char *const textStart + = ((const char *)entity->textPtr) + entity->processed; + const char *const textEnd + = (const char *)(entity->textPtr + entity->textLen); + /* Set a safe default value in case 'next' does not get set */ + const char *nextInEntity = textStart; + if (entity->hasMore) { + result = appendAttributeValue( + parser, parser->m_internalEncoding, isCdata, textStart, textEnd, + pool, XML_ACCOUNT_ENTITY_EXPANSION, &nextInEntity); + if (result != XML_ERROR_NONE) + break; + // Check if entity is complete, if not, mark down how much of it is + // processed. A XML_SUSPENDED check here is not required as + // appendAttributeValue will never suspend the parser. + if (nextInEntity < textEnd) { + entity->processed + = (int)(nextInEntity - (const char *)entity->textPtr); + continue; + } + assert(nextInEntity == textEnd); + + // Entity is complete. We cannot close it here since we need to first + // process its possible inner entities (which are added to the + // m_openAttributeEntities during appendAttributeValue) + entity->hasMore = false; + continue; + } // End of entity processing, "if" block skips the rest + + // Remove fully processed openEntity from open entity list. +#if XML_GE == 1 + entityTrackingOnClose(parser, entity, __LINE__); +#endif + // openEntity is m_openAttributeEntities' head, since we set it at the + // start of this function and because we skipped appendAttributeValue call + // with hasMore set to false. This means we can directly remove the head + // of m_openAttributeEntities + assert(parser->m_openAttributeEntities == openEntity); + entity->open = false; + parser->m_openAttributeEntities = parser->m_openAttributeEntities->next; + + /* put openEntity back in list of free instances */ + openEntity->next = parser->m_freeEntities; + parser->m_freeEntities = openEntity; + } + + // Break if an error occurred or there is nothing left to process + if (result || (parser->m_openAttributeEntities == NULL && end == next)) { + break; + } + } + + if (result) + return result; + if (! isCdata && poolLength(pool) && poolLastChar(pool) == 0x20) + poolChop(pool); + if (! poolAppendChar(pool, XML_T('\0'))) + return XML_ERROR_NO_MEMORY; + return XML_ERROR_NONE; +} + +static enum XML_Error +appendAttributeValue(XML_Parser parser, const ENCODING *enc, XML_Bool isCdata, + const char *ptr, const char *end, STRING_POOL *pool, + enum XML_Account account, const char **nextPtr) { + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ +#ifndef XML_DTD + UNUSED_P(account); +#endif + + for (;;) { + const char *next + = ptr; /* XmlAttributeValueTok doesn't always set the last arg */ + int tok = XmlAttributeValueTok(enc, ptr, end, &next); +#if XML_GE == 1 + if (! accountingDiffTolerated(parser, tok, ptr, next, __LINE__, account)) { + accountingOnAbort(parser); + return XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + } +#endif + switch (tok) { + case XML_TOK_NONE: + if (nextPtr) { + *nextPtr = next; + } + return XML_ERROR_NONE; + case XML_TOK_INVALID: + if (enc == parser->m_encoding) + parser->m_eventPtr = next; + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_PARTIAL: + if (enc == parser->m_encoding) + parser->m_eventPtr = ptr; + return XML_ERROR_INVALID_TOKEN; + case XML_TOK_CHAR_REF: { + XML_Char buf[XML_ENCODE_MAX]; + int n = XmlCharRefNumber(enc, ptr); + if (n < 0) { + if (enc == parser->m_encoding) + parser->m_eventPtr = ptr; + return XML_ERROR_BAD_CHAR_REF; + } + if (! isCdata && n == 0x20 /* space */ + && (poolLength(pool) == 0 || poolLastChar(pool) == 0x20)) + break; + n = XmlEncode(n, (ICHAR *)buf); + /* The XmlEncode() functions can never return 0 here. That + * error return happens if the code point passed in is either + * negative or greater than or equal to 0x110000. The + * XmlCharRefNumber() functions will all return a number + * strictly less than 0x110000 or a negative value if an error + * occurred. The negative value is intercepted above, so + * XmlEncode() is never passed a value it might return an + * error for. + */ + + if (! poolAppendChars(pool, buf, n)) + return XML_ERROR_NO_MEMORY; + } break; + case XML_TOK_DATA_CHARS: + if (! poolAppend(pool, enc, ptr, next)) + return XML_ERROR_NO_MEMORY; + break; + case XML_TOK_TRAILING_CR: + next = ptr + enc->minBytesPerChar; + EXPAT_FALLTHROUGH; + case XML_TOK_ATTRIBUTE_VALUE_S: + case XML_TOK_DATA_NEWLINE: + if (! isCdata && (poolLength(pool) == 0 || poolLastChar(pool) == 0x20)) + break; + if (! poolAppendChar(pool, 0x20)) + return XML_ERROR_NO_MEMORY; + break; + case XML_TOK_ENTITY_REF: { + const XML_Char *name; + ENTITY *entity; + bool checkEntityDecl; + XML_Char ch = (XML_Char)XmlPredefinedEntityName( + enc, ptr + enc->minBytesPerChar, next - enc->minBytesPerChar); + if (ch) { +#if XML_GE == 1 + /* NOTE: We are replacing 4-6 characters original input for 1 character + * so there is no amplification and hence recording without + * protection. */ + accountingDiffTolerated(parser, tok, (char *)&ch, + ((char *)&ch) + sizeof(XML_Char), __LINE__, + XML_ACCOUNT_ENTITY_EXPANSION); +#endif /* XML_GE == 1 */ + if (! poolAppendChar(pool, ch)) + return XML_ERROR_NO_MEMORY; + break; + } + name = poolStoreString(&parser->m_temp2Pool, enc, + ptr + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! name) + return XML_ERROR_NO_MEMORY; + entity = (ENTITY *)lookup(parser, &dtd->generalEntities, name, 0); + poolDiscard(&parser->m_temp2Pool); + /* First, determine if a check for an existing declaration is needed; + if yes, check that the entity exists, and that it is internal. + */ + if (pool == &dtd->pool) /* are we called from prolog? */ + checkEntityDecl = +#ifdef XML_DTD + parser->m_prologState.documentEntity && +#endif /* XML_DTD */ + (dtd->standalone ? ! parser->m_openInternalEntities + : ! dtd->hasParamEntityRefs); + else /* if (pool == &parser->m_tempPool): we are called from content */ + checkEntityDecl = ! dtd->hasParamEntityRefs || dtd->standalone; + if (checkEntityDecl) { + if (! entity) + return XML_ERROR_UNDEFINED_ENTITY; + else if (! entity->is_internal) + return XML_ERROR_ENTITY_DECLARED_IN_PE; + } else if (! entity) { + /* Cannot report skipped entity here - see comments on + parser->m_skippedEntityHandler. + if (parser->m_skippedEntityHandler) { + beforeHandler(parser); + parser->m_skippedEntityHandler(parser->m_handlerArg, name, 0); + afterHandler(parser); + } + */ + /* Cannot call the default handler because this would be + out of sync with the call to the startElementHandler. + if ((pool == &parser->m_tempPool) && parser->m_defaultHandler) + reportDefault(parser, enc, ptr, next); + */ + break; + } + if (entity->open) { + if (enc == parser->m_encoding) { + /* It does not appear that this line can be executed. + * + * The "if (entity->open)" check catches recursive entity + * definitions. In order to be called with an open + * entity, it must have gone through this code before and + * been through the recursive call to + * appendAttributeValue() some lines below. That call + * sets the local encoding ("enc") to the parser's + * internal encoding (internal_utf8 or internal_utf16), + * which can never be the same as the principle encoding. + * It doesn't appear there is another code path that gets + * here with entity->open being TRUE. + * + * Since it is not certain that this logic is watertight, + * we keep the line and merely exclude it from coverage + * tests. + */ + parser->m_eventPtr = ptr; /* LCOV_EXCL_LINE */ + } + return XML_ERROR_RECURSIVE_ENTITY_REF; + } + if (entity->notation) { + if (enc == parser->m_encoding) + parser->m_eventPtr = ptr; + return XML_ERROR_BINARY_ENTITY_REF; + } + if (! entity->textPtr) { + if (enc == parser->m_encoding) + parser->m_eventPtr = ptr; + return XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF; + } else { + enum XML_Error result; + result = processEntity(parser, entity, XML_FALSE, ENTITY_ATTRIBUTE); + if ((result == XML_ERROR_NONE) && (nextPtr != NULL)) { + *nextPtr = next; + } + return result; + } + } break; + default: + /* The only token returned by XmlAttributeValueTok() that does + * not have an explicit case here is XML_TOK_PARTIAL_CHAR. + * Getting that would require an entity name to contain an + * incomplete XML character (e.g. \xE2\x82); however previous + * tokenisers will have already recognised and rejected such + * names before XmlAttributeValueTok() gets a look-in. This + * default case should be retained as a safety net, but the code + * excluded from coverage tests. + * + * LCOV_EXCL_START + */ + if (enc == parser->m_encoding) + parser->m_eventPtr = ptr; + return XML_ERROR_UNEXPECTED_STATE; + /* LCOV_EXCL_STOP */ + } + ptr = next; + } + /* not reached */ +} + +#if XML_GE == 1 +static enum XML_Error +storeEntityValue(XML_Parser parser, const ENCODING *enc, + const char *entityTextPtr, const char *entityTextEnd, + enum XML_Account account, const char **nextPtr) { + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + STRING_POOL *pool = &(dtd->entityValuePool); + enum XML_Error result = XML_ERROR_NONE; +# ifdef XML_DTD + int oldInEntityValue = parser->m_prologState.inEntityValue; + parser->m_prologState.inEntityValue = 1; +# else + UNUSED_P(account); +# endif /* XML_DTD */ + /* never return Null for the value argument in EntityDeclHandler, + since this would indicate an external entity; therefore we + have to make sure that entityValuePool.start is not null */ + if (! pool->blocks) { + if (! poolGrow(pool)) + return XML_ERROR_NO_MEMORY; + } + + const char *next = entityTextPtr; + + /* Nothing to tokenize. */ + if (entityTextPtr >= entityTextEnd) { + result = XML_ERROR_NONE; + goto endEntityValue; + } + + for (;;) { + next + = entityTextPtr; /* XmlEntityValueTok doesn't always set the last arg */ + int tok = XmlEntityValueTok(enc, entityTextPtr, entityTextEnd, &next); + + if (! accountingDiffTolerated(parser, tok, entityTextPtr, next, __LINE__, + account)) { + accountingOnAbort(parser); + result = XML_ERROR_AMPLIFICATION_LIMIT_BREACH; + goto endEntityValue; + } + + switch (tok) { + case XML_TOK_PARAM_ENTITY_REF: +# ifdef XML_DTD + if (parser->m_isParamEntity || enc != parser->m_encoding) { + const XML_Char *name; + ENTITY *entity; + name = poolStoreString(&parser->m_tempPool, enc, + entityTextPtr + enc->minBytesPerChar, + next - enc->minBytesPerChar); + if (! name) { + result = XML_ERROR_NO_MEMORY; + goto endEntityValue; + } + entity = (ENTITY *)lookup(parser, &dtd->paramEntities, name, 0); + poolDiscard(&parser->m_tempPool); + if (! entity) { + /* not a well-formedness error - see XML 1.0: WFC Entity Declared */ + /* cannot report skipped entity here - see comments on + parser->m_skippedEntityHandler + if (parser->m_skippedEntityHandler) { + beforeHandler(parser); + parser->m_skippedEntityHandler(parser->m_handlerArg, name, 0); + afterHandler(parser); + } + */ + dtd->keepProcessing = dtd->standalone; + goto endEntityValue; + } + if (entity->open || (entity == parser->m_declEntity)) { + if (enc == parser->m_encoding) + parser->m_eventPtr = entityTextPtr; + result = XML_ERROR_RECURSIVE_ENTITY_REF; + goto endEntityValue; + } + if (entity->systemId) { + if (parser->m_externalEntityRefHandler) { + dtd->paramEntityRead = false; + entity->open = true; + entityTrackingOnOpen(parser, entity, __LINE__); + beforeHandler(parser); + const int status = parser->m_externalEntityRefHandler( + parser->m_externalEntityRefHandlerArg, 0, entity->base, + entity->systemId, entity->publicId); + afterHandler(parser); + if (! status) { + entityTrackingOnClose(parser, entity, __LINE__); + entity->open = false; + result = XML_ERROR_EXTERNAL_ENTITY_HANDLING; + goto endEntityValue; + } + entityTrackingOnClose(parser, entity, __LINE__); + entity->open = false; + if (! dtd->paramEntityRead) + dtd->keepProcessing = dtd->standalone; + } else + dtd->keepProcessing = dtd->standalone; + } else { + result = processEntity(parser, entity, XML_FALSE, ENTITY_VALUE); + goto endEntityValue; + } + break; + } +# endif /* XML_DTD */ + /* In the internal subset, PE references are not legal + within markup declarations, e.g entity values in this case. */ + parser->m_eventPtr = entityTextPtr; + result = XML_ERROR_PARAM_ENTITY_REF; + goto endEntityValue; + case XML_TOK_NONE: + result = XML_ERROR_NONE; + goto endEntityValue; + case XML_TOK_ENTITY_REF: + case XML_TOK_DATA_CHARS: + if (! poolAppend(pool, enc, entityTextPtr, next)) { + result = XML_ERROR_NO_MEMORY; + goto endEntityValue; + } + break; + case XML_TOK_TRAILING_CR: + next = entityTextPtr + enc->minBytesPerChar; + EXPAT_FALLTHROUGH; + case XML_TOK_DATA_NEWLINE: + if (! poolAppendChar(pool, 0xA)) { + result = XML_ERROR_NO_MEMORY; + goto endEntityValue; + } + break; + case XML_TOK_CHAR_REF: { + XML_Char buf[XML_ENCODE_MAX]; + int n = XmlCharRefNumber(enc, entityTextPtr); + if (n < 0) { + if (enc == parser->m_encoding) + parser->m_eventPtr = entityTextPtr; + result = XML_ERROR_BAD_CHAR_REF; + goto endEntityValue; + } + n = XmlEncode(n, (ICHAR *)buf); + /* The XmlEncode() functions can never return 0 here. That + * error return happens if the code point passed in is either + * negative or greater than or equal to 0x110000. The + * XmlCharRefNumber() functions will all return a number + * strictly less than 0x110000 or a negative value if an error + * occurred. The negative value is intercepted above, so + * XmlEncode() is never passed a value it might return an + * error for. + */ + if (! poolAppendChars(pool, buf, n)) { + result = XML_ERROR_NO_MEMORY; + goto endEntityValue; + } + } break; + case XML_TOK_PARTIAL: + if (enc == parser->m_encoding) + parser->m_eventPtr = entityTextPtr; + result = XML_ERROR_INVALID_TOKEN; + goto endEntityValue; + case XML_TOK_INVALID: + if (enc == parser->m_encoding) + parser->m_eventPtr = next; + result = XML_ERROR_INVALID_TOKEN; + goto endEntityValue; + default: + /* This default case should be unnecessary -- all the tokens + * that XmlEntityValueTok() can return have their own explicit + * cases -- but should be retained for safety. We do however + * exclude it from the coverage statistics. + * + * LCOV_EXCL_START + */ + if (enc == parser->m_encoding) + parser->m_eventPtr = entityTextPtr; + result = XML_ERROR_UNEXPECTED_STATE; + goto endEntityValue; + /* LCOV_EXCL_STOP */ + } + entityTextPtr = next; + } +endEntityValue: +# ifdef XML_DTD + parser->m_prologState.inEntityValue = oldInEntityValue; +# endif /* XML_DTD */ + // If 'nextPtr' is given, it should be updated during the processing + if (nextPtr != NULL) { + *nextPtr = next; + } + return result; +} + +static enum XML_Error +callStoreEntityValue(XML_Parser parser, const ENCODING *enc, + const char *entityTextPtr, const char *entityTextEnd, + enum XML_Account account) { + const char *next = entityTextPtr; + enum XML_Error result = XML_ERROR_NONE; + while (1) { + if (! parser->m_openValueEntities) { + result + = storeEntityValue(parser, enc, next, entityTextEnd, account, &next); + } else { + OPEN_INTERNAL_ENTITY *const openEntity = parser->m_openValueEntities; + if (! openEntity) + return XML_ERROR_UNEXPECTED_STATE; + + ENTITY *const entity = openEntity->entity; + const char *const textStart + = ((const char *)entity->textPtr) + entity->processed; + const char *const textEnd + = (const char *)(entity->textPtr + entity->textLen); + /* Set a safe default value in case 'next' does not get set */ + const char *nextInEntity = textStart; + if (entity->hasMore) { + result = storeEntityValue(parser, parser->m_internalEncoding, textStart, + textEnd, XML_ACCOUNT_ENTITY_EXPANSION, + &nextInEntity); + if (result != XML_ERROR_NONE) + break; + // Check if entity is complete, if not, mark down how much of it is + // processed. A XML_SUSPENDED check here is not required as + // appendAttributeValue will never suspend the parser. + if (textEnd != nextInEntity) { + entity->processed + = (int)(nextInEntity - (const char *)entity->textPtr); + continue; + } + + // Entity is complete. We cannot close it here since we need to first + // process its possible inner entities (which are added to the + // m_openValueEntities during storeEntityValue) + entity->hasMore = false; + continue; + } // End of entity processing, "if" block skips the rest + + // Remove fully processed openEntity from open entity list. +# if XML_GE == 1 + entityTrackingOnClose(parser, entity, __LINE__); +# endif + // openEntity is m_openValueEntities' head, since we set it at the + // start of this function and because we skipped storeEntityValue call + // with hasMore set to false. This means we can directly remove the head + // of m_openValueEntities + assert(parser->m_openValueEntities == openEntity); + entity->open = false; + parser->m_openValueEntities = parser->m_openValueEntities->next; + + /* put openEntity back in list of free instances */ + openEntity->next = parser->m_freeEntities; + parser->m_freeEntities = openEntity; + } + + // Break if an error occurred or there is nothing left to process + if (result + || (parser->m_openValueEntities == NULL && entityTextEnd == next)) { + break; + } + } + + return result; +} + +#else /* XML_GE == 0 */ + +static enum XML_Error +storeSelfEntityValue(XML_Parser parser, ENTITY *entity) { + // This will store "&entity123;" in entity->textPtr + // to end up as "&entity123;" in the handler. + const char *const entity_start = "&"; + const char *const entity_end = ";"; + + STRING_POOL *const pool = &(parser->m_dtd->entityValuePool); + if (! poolAppendString(pool, entity_start) + || ! poolAppendString(pool, entity->name) + || ! poolAppendString(pool, entity_end)) { + poolDiscard(pool); + return XML_ERROR_NO_MEMORY; + } + + /* Detect and prevent signed integer overflow */ + if ((size_t)poolLength(pool) > (size_t)INT_MAX) { + poolDiscard(pool); + return XML_ERROR_NO_MEMORY; + } + entity->textPtr = poolStart(pool); + entity->textLen = (int)(poolLength(pool)); + poolFinish(pool); + + return XML_ERROR_NONE; +} + +#endif /* XML_GE == 0 */ + +static void +normalizeLines(XML_Char *s) { + XML_Char *p; + for (;; s++) { + if (*s == XML_T('\0')) + return; + if (*s == 0xD) + break; + } + p = s; + do { + if (*s == 0xD) { + *p++ = 0xA; + if (*++s == 0xA) + s++; + } else + *p++ = *s++; + } while (*s); + *p = XML_T('\0'); +} + +static int +reportProcessingInstruction(XML_Parser parser, const ENCODING *enc, + const char *start, const char *end) { + const XML_Char *target; + XML_Char *data; + const char *tem; + if (! parser->m_processingInstructionHandler) { + if (parser->m_defaultHandler) + reportDefault(parser, enc, start, end); + return 1; + } + start += enc->minBytesPerChar * 2; + tem = start + XmlNameLength(enc, start); + target = poolStoreString(&parser->m_tempPool, enc, start, tem); + if (! target) + return 0; + poolFinish(&parser->m_tempPool); + data = poolStoreString(&parser->m_tempPool, enc, XmlSkipS(enc, tem), + end - enc->minBytesPerChar * 2); + if (! data) + return 0; + normalizeLines(data); + beforeHandler(parser); + parser->m_processingInstructionHandler(parser->m_handlerArg, target, data); + afterHandler(parser); + poolClear(&parser->m_tempPool); + return 1; +} + +static int +reportComment(XML_Parser parser, const ENCODING *enc, const char *start, + const char *end) { + XML_Char *data; + if (! parser->m_commentHandler) { + if (parser->m_defaultHandler) + reportDefault(parser, enc, start, end); + return 1; + } + data = poolStoreString(&parser->m_tempPool, enc, + start + enc->minBytesPerChar * 4, + end - enc->minBytesPerChar * 3); + if (! data) + return 0; + normalizeLines(data); + beforeHandler(parser); + parser->m_commentHandler(parser->m_handlerArg, data); + afterHandler(parser); + poolClear(&parser->m_tempPool); + return 1; +} + +static void +reportDefault(XML_Parser parser, const ENCODING *enc, const char *s, + const char *end) { + if (MUST_CONVERT(enc, s)) { + enum XML_Convert_Result convert_res; + const char **eventPP; + const char **eventEndPP; + if (enc == parser->m_encoding) { + eventPP = &parser->m_eventPtr; + eventEndPP = &parser->m_eventEndPtr; + } else { + /* To get here, two things must be true; the parser must be + * using a character encoding that is not the same as the + * encoding passed in, and the encoding passed in must need + * conversion to the internal format (UTF-8 unless XML_UNICODE + * is defined). The only occasions on which the encoding passed + * in is not the same as the parser's encoding are when it is + * the internal encoding (e.g. a previously defined parameter + * entity, already converted to internal format). This by + * definition doesn't need conversion, so the whole branch never + * gets executed. + * + * For safety's sake we don't delete these lines and merely + * exclude them from coverage statistics. + * + * LCOV_EXCL_START + */ + eventPP = &(parser->m_openInternalEntities->internalEventPtr); + eventEndPP = &(parser->m_openInternalEntities->internalEventEndPtr); + /* LCOV_EXCL_STOP */ + } + do { + ICHAR *dataPtr = (ICHAR *)parser->m_dataBuf; + convert_res + = XmlConvert(enc, &s, end, &dataPtr, (ICHAR *)parser->m_dataBufEnd); + *eventEndPP = s; + beforeHandler(parser); + parser->m_defaultHandler(parser->m_handlerArg, parser->m_dataBuf, + (int)(dataPtr - (ICHAR *)parser->m_dataBuf)); + afterHandler(parser); + *eventPP = s; + } while ((convert_res != XML_CONVERT_COMPLETED) + && (convert_res != XML_CONVERT_INPUT_INCOMPLETE)); + } else { + beforeHandler(parser); + parser->m_defaultHandler( + parser->m_handlerArg, (const XML_Char *)s, + (int)((const XML_Char *)end - (const XML_Char *)s)); + afterHandler(parser); + } +} + +static int +defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata, + XML_Bool isId, const XML_Char *value, XML_Parser parser) { + DEFAULT_ATTRIBUTE *att; + if (value || isId) { + /* The handling of default attributes gets messed up if we have + a default which duplicates a non-default. */ + NAMED *const nameFound + = lookup(parser, &(type->defaultAttForName), attId->name, 0); + if (nameFound) + return 1; + if (isId && ! type->idAtt && ! attId->xmlns) + type->idAtt = attId; + } + if (type->nDefaultAtts == type->allocDefaultAtts) { + /* Detect and prevent integer overflow */ + if (type->allocDefaultAtts > SIZE_MAX / 2) { + return 0; + } + + size_t count = type->allocDefaultAtts * 2; + if (count == 0) { + count = 8; + } + + /* Detect and prevent integer overflow. */ + if (count > SIZE_MAX / sizeof(DEFAULT_ATTRIBUTE)) { + return 0; + } + + DEFAULT_ATTRIBUTE *const temp = REALLOC( + parser, type->defaultAtts, (count * sizeof(DEFAULT_ATTRIBUTE))); + if (temp == NULL) + return 0; + type->allocDefaultAtts = count; + type->defaultAtts = temp; + } + att = type->defaultAtts + type->nDefaultAtts; + att->id = attId; + att->value = value; + att->isCdata = isCdata; + if (! isCdata) + attId->maybeTokenized = true; + + NAME_AND_DEFAULT_ATTRIBUTE *const nameAndDefaultAttribute + = (NAME_AND_DEFAULT_ATTRIBUTE *)lookup( + parser, &(type->defaultAttForName), attId->name, + sizeof(NAME_AND_DEFAULT_ATTRIBUTE)); + if (! nameAndDefaultAttribute) + return 0; + + assert(nameAndDefaultAttribute->name == attId->name); + + // NOTE: The XML 1.0r4 spec says: + // "When more than one definition is provided for the same attribute of a + // given element type, the first declaration is binding and later + // declarations are ignored." + if (! nameAndDefaultAttribute->initialized) { + nameAndDefaultAttribute->attIndex = type->nDefaultAtts; + nameAndDefaultAttribute->initialized = true; + } + + type->nDefaultAtts += 1; + return 1; +} + +static int +setElementTypePrefix(XML_Parser parser, ELEMENT_TYPE *elementType) { + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + const XML_Char *name; + for (name = elementType->name; *name; name++) { + if (*name == XML_T(ASCII_COLON)) { + PREFIX *prefix; + const XML_Char *s; + for (s = elementType->name; s != name; s++) { + if (! poolAppendChar(&dtd->pool, *s)) + return 0; + } + if (! poolAppendChar(&dtd->pool, XML_T('\0'))) + return 0; + prefix = (PREFIX *)lookup(parser, &dtd->prefixes, poolStart(&dtd->pool), + sizeof(PREFIX)); + if (! prefix) + return 0; + if (prefix->name == poolStart(&dtd->pool)) + poolFinish(&dtd->pool); + else + poolDiscard(&dtd->pool); + elementType->prefix = prefix; + break; + } + } + return 1; +} + +static ATTRIBUTE_ID * +getAttributeId(XML_Parser parser, const ENCODING *enc, const char *start, + const char *end) { + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + ATTRIBUTE_ID *id; + const XML_Char *name; + if (! poolAppendChar(&dtd->pool, XML_T('\0'))) + return NULL; + name = poolStoreString(&dtd->pool, enc, start, end); + if (! name) + return NULL; + /* skip quotation mark - its storage will be reused (like in name[-1]) */ + ++name; + id = (ATTRIBUTE_ID *)lookup(parser, &dtd->attributeIds, name, + sizeof(ATTRIBUTE_ID)); + if (! id) + return NULL; + if (id->name != name) + poolDiscard(&dtd->pool); + else { + poolFinish(&dtd->pool); + if (! parser->m_ns) + ; + else if (name[0] == XML_T(ASCII_x) && name[1] == XML_T(ASCII_m) + && name[2] == XML_T(ASCII_l) && name[3] == XML_T(ASCII_n) + && name[4] == XML_T(ASCII_s) + && (name[5] == XML_T('\0') || name[5] == XML_T(ASCII_COLON))) { + if (name[5] == XML_T('\0')) + id->prefix = &dtd->defaultPrefix; + else + id->prefix = (PREFIX *)lookup(parser, &dtd->prefixes, name + 6, + sizeof(PREFIX)); + id->xmlns = true; + } else { + int i; + for (i = 0; name[i]; i++) { + /* Detect and prevent signed integer overflow */ + if (i == INT_MAX) { + return NULL; + } + /* attributes without prefix are *not* in the default namespace */ + if (name[i] == XML_T(ASCII_COLON)) { + if (! poolAppendChars(&dtd->pool, name, i)) + return NULL; + if (! poolAppendChar(&dtd->pool, XML_T('\0'))) + return NULL; + id->prefix = (PREFIX *)lookup(parser, &dtd->prefixes, + poolStart(&dtd->pool), sizeof(PREFIX)); + if (! id->prefix) + return NULL; + if (id->prefix->name == poolStart(&dtd->pool)) + poolFinish(&dtd->pool); + else + poolDiscard(&dtd->pool); + break; + } + } + } + } + return id; +} + +#define CONTEXT_SEP XML_T(ASCII_FF) + +static const XML_Char * +getContext(XML_Parser parser) { + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + HASH_TABLE_ITER iter; + XML_Bool needSep = XML_FALSE; + + if (dtd->defaultPrefix.binding) { + if (! poolAppendChar(&parser->m_tempPool, XML_T(ASCII_EQUALS))) + return NULL; + size_t len = dtd->defaultPrefix.binding->uriLen; + if (parser->m_namespaceSeparator) + len--; + if (! poolAppendChars(&parser->m_tempPool, dtd->defaultPrefix.binding->uri, + len)) { + /* Because of memory caching, I don't believe this line can be + * executed. + * + * This is part of a loop copying the default prefix binding + * URI into the parser's temporary string pool. Previously, + * that URI was copied into the same string pool, with a + * terminating NUL character, as part of setContext(). When + * the pool was cleared, that leaves a block definitely big + * enough to hold the URI on the free block list of the pool. + * The URI copy in getContext() therefore cannot run out of + * memory. + * + * If the pool is used between the setContext() and + * getContext() calls, the worst it can do is leave a bigger + * block on the front of the free list. Given that this is + * all somewhat inobvious and program logic can be changed, we + * don't delete the line but we do exclude it from the test + * coverage statistics. + */ + return NULL; /* LCOV_EXCL_LINE */ + } + needSep = XML_TRUE; + } + + hashTableIterInit(&iter, &(dtd->prefixes)); + for (;;) { + PREFIX *prefix = (PREFIX *)hashTableIterNext(&iter); + if (! prefix) + break; + if (! prefix->binding) { + /* This test appears to be (justifiable) paranoia. There does + * not seem to be a way of injecting a prefix without a binding + * that doesn't get errored long before this function is called. + * The test should remain for safety's sake, so we instead + * exclude the following line from the coverage statistics. + */ + continue; /* LCOV_EXCL_LINE */ + } + if (needSep && ! poolAppendChar(&parser->m_tempPool, CONTEXT_SEP)) + return NULL; + if (! poolAppendChars(&parser->m_tempPool, prefix->name, + xcslen(prefix->name))) + return NULL; + if (! poolAppendChar(&parser->m_tempPool, XML_T(ASCII_EQUALS))) + return NULL; + size_t len = prefix->binding->uriLen; + if (parser->m_namespaceSeparator) + len--; + if (! poolAppendChars(&parser->m_tempPool, prefix->binding->uri, len)) + return NULL; + needSep = XML_TRUE; + } + + hashTableIterInit(&iter, &(dtd->generalEntities)); + for (;;) { + ENTITY *e = (ENTITY *)hashTableIterNext(&iter); + if (! e) + break; + if (! e->open) + continue; + if (needSep && ! poolAppendChar(&parser->m_tempPool, CONTEXT_SEP)) + return NULL; + if (! poolAppendChars(&parser->m_tempPool, e->name, xcslen(e->name))) + return NULL; + needSep = XML_TRUE; + } + + if (! poolAppendChar(&parser->m_tempPool, XML_T('\0'))) + return NULL; + return parser->m_tempPool.start; +} + +static XML_Bool +setContext(XML_Parser parser, const XML_Char *context) { + if (context == NULL) { + return XML_FALSE; + } + + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + const XML_Char *s = context; + + while (*context != XML_T('\0')) { + if (*s == CONTEXT_SEP || *s == XML_T('\0')) { + ENTITY *e; + if (! poolAppendChar(&parser->m_tempPool, XML_T('\0'))) + return XML_FALSE; + e = (ENTITY *)lookup(parser, &dtd->generalEntities, + poolStart(&parser->m_tempPool), 0); + if (e) + e->open = true; + if (*s != XML_T('\0')) + s++; + context = s; + poolDiscard(&parser->m_tempPool); + } else if (*s == XML_T(ASCII_EQUALS)) { + PREFIX *prefix; + if (poolLength(&parser->m_tempPool) == 0) + prefix = &dtd->defaultPrefix; + else { + if (! poolAppendChar(&parser->m_tempPool, XML_T('\0'))) + return XML_FALSE; + const XML_Char *const prefixName = poolCopyStringNoFinish( + &dtd->pool, poolStart(&parser->m_tempPool)); + if (! prefixName) { + return XML_FALSE; + } + + prefix = (PREFIX *)lookup(parser, &dtd->prefixes, prefixName, + sizeof(PREFIX)); + + const bool prefixNameUsed = prefix && prefix->name == prefixName; + if (prefixNameUsed) + poolFinish(&dtd->pool); + else + poolDiscard(&dtd->pool); + + if (! prefix) + return XML_FALSE; + + poolDiscard(&parser->m_tempPool); + } + for (context = s + 1; *context != CONTEXT_SEP && *context != XML_T('\0'); + context++) + if (! poolAppendChar(&parser->m_tempPool, *context)) + return XML_FALSE; + if (! poolAppendChar(&parser->m_tempPool, XML_T('\0'))) + return XML_FALSE; + if (addBinding(parser, prefix, NULL, poolStart(&parser->m_tempPool), + &parser->m_inheritedBindings) + != XML_ERROR_NONE) + return XML_FALSE; + poolDiscard(&parser->m_tempPool); + if (*context != XML_T('\0')) + ++context; + s = context; + } else { + if (! poolAppendChar(&parser->m_tempPool, *s)) + return XML_FALSE; + s++; + } + } + return XML_TRUE; +} + +static void +normalizePublicId(XML_Char *publicId) { + XML_Char *p = publicId; + XML_Char *s; + for (s = publicId; *s; s++) { + switch (*s) { + case 0x20: + case 0xD: + case 0xA: + if (p != publicId && p[-1] != 0x20) + *p++ = 0x20; + break; + default: + *p++ = *s; + } + } + if (p != publicId && p[-1] == 0x20) + --p; + *p = XML_T('\0'); +} + +static DTD * +dtdCreate(XML_Parser parser) { + DTD *p = MALLOC(parser, sizeof(DTD)); + if (p == NULL) + return p; + poolInit(&(p->pool), parser); + poolInit(&(p->entityValuePool), parser); + hashTableInit(&(p->generalEntities), parser); + hashTableInit(&(p->elementTypes), parser); + hashTableInit(&(p->attributeIds), parser); + hashTableInit(&(p->prefixes), parser); +#ifdef XML_DTD + p->paramEntityRead = false; + hashTableInit(&(p->paramEntities), parser); +#endif /* XML_DTD */ + p->defaultPrefix.name = NULL; + p->defaultPrefix.binding = NULL; + + p->in_eldecl = false; + p->scaffIndex = NULL; + p->scaffIndexSize = 0; + p->scaffold = NULL; + p->scaffLevel = 0; + p->scaffSize = 0; + p->scaffCount = 0; + p->contentStringLen = 0; + + p->keepProcessing = XML_TRUE; + p->hasParamEntityRefs = XML_FALSE; + p->standalone = XML_FALSE; + return p; +} + +static void +dtdReset(DTD *p, XML_Parser parser) { + HASH_TABLE_ITER iter; + hashTableIterInit(&iter, &(p->elementTypes)); + for (;;) { + ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter); + if (! e) + break; + hashTableDestroy(&(e->defaultAttForName)); + FREE(parser, e->defaultAtts); + } + hashTableClear(&(p->generalEntities)); +#ifdef XML_DTD + p->paramEntityRead = false; + hashTableClear(&(p->paramEntities)); +#endif /* XML_DTD */ + hashTableClear(&(p->elementTypes)); + hashTableClear(&(p->attributeIds)); + hashTableClear(&(p->prefixes)); + poolClear(&(p->pool)); + poolClear(&(p->entityValuePool)); + p->defaultPrefix.name = NULL; + p->defaultPrefix.binding = NULL; + + p->in_eldecl = false; + + FREE(parser, p->scaffIndex); + p->scaffIndex = NULL; + p->scaffIndexSize = 0; + FREE(parser, p->scaffold); + p->scaffold = NULL; + + p->scaffLevel = 0; + p->scaffSize = 0; + p->scaffCount = 0; + p->contentStringLen = 0; + + p->keepProcessing = XML_TRUE; + p->hasParamEntityRefs = XML_FALSE; + p->standalone = XML_FALSE; +} + +static void +dtdDestroy(DTD *p, XML_Bool isDocEntity, XML_Parser parser) { + HASH_TABLE_ITER iter; + hashTableIterInit(&iter, &(p->elementTypes)); + for (;;) { + ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter); + if (! e) + break; + hashTableDestroy(&(e->defaultAttForName)); + FREE(parser, e->defaultAtts); + } + hashTableDestroy(&(p->generalEntities)); +#ifdef XML_DTD + hashTableDestroy(&(p->paramEntities)); +#endif /* XML_DTD */ + hashTableDestroy(&(p->elementTypes)); + hashTableDestroy(&(p->attributeIds)); + hashTableDestroy(&(p->prefixes)); + poolDestroy(&(p->pool)); + poolDestroy(&(p->entityValuePool)); + if (isDocEntity) { + FREE(parser, p->scaffIndex); + FREE(parser, p->scaffold); + } + FREE(parser, p); +} + +/* Do a deep copy of the DTD. Return 0 for out of memory, non-zero otherwise. + The new DTD has already been initialized. +*/ +static int +dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd, + XML_Parser parser) { + HASH_TABLE_ITER iter; + + /* Copy the prefix table. */ + + hashTableIterInit(&iter, &(oldDtd->prefixes)); + for (;;) { + const XML_Char *name; + const PREFIX *oldP = (PREFIX *)hashTableIterNext(&iter); + if (! oldP) + break; + name = poolCopyString(&(newDtd->pool), oldP->name); + if (! name) + return 0; + if (! lookup(oldParser, &(newDtd->prefixes), name, sizeof(PREFIX))) + return 0; + } + + hashTableIterInit(&iter, &(oldDtd->attributeIds)); + + /* Copy the attribute id table. */ + + for (;;) { + ATTRIBUTE_ID *newA; + const XML_Char *name; + const ATTRIBUTE_ID *oldA = (ATTRIBUTE_ID *)hashTableIterNext(&iter); + + if (! oldA) + break; + /* Remember to allocate the scratch byte before the name. */ + if (! poolAppendChar(&(newDtd->pool), XML_T('\0'))) + return 0; + name = poolCopyString(&(newDtd->pool), oldA->name); + if (! name) + return 0; + ++name; + newA = (ATTRIBUTE_ID *)lookup(oldParser, &(newDtd->attributeIds), name, + sizeof(ATTRIBUTE_ID)); + if (! newA) + return 0; + newA->maybeTokenized = oldA->maybeTokenized; + if (oldA->prefix) { + newA->xmlns = oldA->xmlns; + if (oldA->prefix == &oldDtd->defaultPrefix) + newA->prefix = &newDtd->defaultPrefix; + else + newA->prefix = (PREFIX *)lookup(oldParser, &(newDtd->prefixes), + oldA->prefix->name, 0); + } + } + + /* Copy the element type table. */ + + hashTableIterInit(&iter, &(oldDtd->elementTypes)); + + for (;;) { + ELEMENT_TYPE *newE; + const XML_Char *name; + const ELEMENT_TYPE *oldE = (ELEMENT_TYPE *)hashTableIterNext(&iter); + if (! oldE) + break; + name = poolCopyString(&(newDtd->pool), oldE->name); + if (! name) + return 0; + newE = (ELEMENT_TYPE *)lookup(oldParser, &(newDtd->elementTypes), name, + sizeof(ELEMENT_TYPE)); + if (! newE) + return 0; + + if (! newE->defaultAttForName.parser) + hashTableInit(&(newE->defaultAttForName), parser); + + if (oldE->nDefaultAtts) { + /* Detect and prevent integer overflow. */ + if (oldE->nDefaultAtts > SIZE_MAX / sizeof(DEFAULT_ATTRIBUTE)) { + return 0; + } + newE->defaultAtts + = MALLOC(parser, oldE->nDefaultAtts * sizeof(DEFAULT_ATTRIBUTE)); + if (! newE->defaultAtts) { + return 0; + } + } + if (oldE->idAtt) + newE->idAtt = (ATTRIBUTE_ID *)lookup(oldParser, &(newDtd->attributeIds), + oldE->idAtt->name, 0); + newE->allocDefaultAtts = newE->nDefaultAtts = oldE->nDefaultAtts; + if (oldE->prefix) + newE->prefix = (PREFIX *)lookup(oldParser, &(newDtd->prefixes), + oldE->prefix->name, 0); + for (size_t i = 0; i < newE->nDefaultAtts; i++) { + const XML_Char *const attributeName = oldE->defaultAtts[i].id->name; + newE->defaultAtts[i].id = (ATTRIBUTE_ID *)lookup( + oldParser, &(newDtd->attributeIds), attributeName, 0); + newE->defaultAtts[i].isCdata = oldE->defaultAtts[i].isCdata; + if (oldE->defaultAtts[i].value) { + newE->defaultAtts[i].value + = poolCopyString(&(newDtd->pool), oldE->defaultAtts[i].value); + if (! newE->defaultAtts[i].value) + return 0; + } else + newE->defaultAtts[i].value = NULL; + + NAME_AND_DEFAULT_ATTRIBUTE *const nameAndDefaultAttribute + = (NAME_AND_DEFAULT_ATTRIBUTE *)lookup( + parser, &(newE->defaultAttForName), attributeName, + sizeof(NAME_AND_DEFAULT_ATTRIBUTE)); + if (! nameAndDefaultAttribute) { + return 0; + } + + // NOTE: The XML 1.0r4 spec says: + // "When more than one definition is provided for the same attribute of a + // given element type, the first declaration is binding and later + // declarations are ignored." + if (! nameAndDefaultAttribute->initialized) { + nameAndDefaultAttribute->attIndex = i; + nameAndDefaultAttribute->initialized = true; + } + } + } + + /* Copy the entity tables. */ + if (! copyEntityTable(oldParser, &(newDtd->generalEntities), &(newDtd->pool), + &(oldDtd->generalEntities))) + return 0; + +#ifdef XML_DTD + if (! copyEntityTable(oldParser, &(newDtd->paramEntities), &(newDtd->pool), + &(oldDtd->paramEntities))) + return 0; + newDtd->paramEntityRead = oldDtd->paramEntityRead; +#endif /* XML_DTD */ + + newDtd->keepProcessing = oldDtd->keepProcessing; + newDtd->hasParamEntityRefs = oldDtd->hasParamEntityRefs; + newDtd->standalone = oldDtd->standalone; + + /* Don't want deep copying for scaffolding */ + newDtd->in_eldecl = oldDtd->in_eldecl; + newDtd->scaffold = oldDtd->scaffold; + newDtd->contentStringLen = oldDtd->contentStringLen; + newDtd->scaffSize = oldDtd->scaffSize; + newDtd->scaffLevel = oldDtd->scaffLevel; + newDtd->scaffIndex = oldDtd->scaffIndex; + newDtd->scaffIndexSize = oldDtd->scaffIndexSize; + + return 1; +} /* End dtdCopy */ + +static int +copyEntityTable(XML_Parser oldParser, HASH_TABLE *newTable, + STRING_POOL *newPool, const HASH_TABLE *oldTable) { + HASH_TABLE_ITER iter; + const XML_Char *cachedOldBase = NULL; + const XML_Char *cachedNewBase = NULL; + + hashTableIterInit(&iter, oldTable); + + for (;;) { + ENTITY *newE; + const XML_Char *name; + const ENTITY *oldE = (ENTITY *)hashTableIterNext(&iter); + if (! oldE) + break; + name = poolCopyString(newPool, oldE->name); + if (! name) + return 0; + newE = (ENTITY *)lookup(oldParser, newTable, name, sizeof(ENTITY)); + if (! newE) + return 0; + if (oldE->systemId) { + const XML_Char *tem = poolCopyString(newPool, oldE->systemId); + if (! tem) + return 0; + newE->systemId = tem; + if (oldE->base) { + if (oldE->base == cachedOldBase) + newE->base = cachedNewBase; + else { + cachedOldBase = oldE->base; + tem = poolCopyString(newPool, cachedOldBase); + if (! tem) + return 0; + cachedNewBase = newE->base = tem; + } + } + if (oldE->publicId) { + tem = poolCopyString(newPool, oldE->publicId); + if (! tem) + return 0; + newE->publicId = tem; + } + } else { + const XML_Char *tem + = poolCopyStringN(newPool, oldE->textPtr, oldE->textLen); + if (! tem) + return 0; + newE->textPtr = tem; + newE->textLen = oldE->textLen; + } + if (oldE->notation) { + const XML_Char *tem = poolCopyString(newPool, oldE->notation); + if (! tem) + return 0; + newE->notation = tem; + } + newE->is_param = oldE->is_param; + newE->is_internal = oldE->is_internal; + } + return 1; +} + +#define INIT_POWER 6 + +// Compares two strings `s1` and `s2` whereas: +// - `s2` is zero-terminated but +// - `s1` is made up of exactly (not just up to) `s1len` non-zero characters. +XML_NONTESTING_STATIC XML_Bool +keyeq(KEY s1, size_t s1len, KEY s2) { +#ifdef XML_UNICODE +# ifdef XML_UNICODE_WCHAR_T + return (wcsncmp(s1, s2, s1len) == 0 && s2[s1len] == L'\0') ? XML_TRUE + : XML_FALSE; +# else + for (; s1len > 0 && *s1 == *s2; s1len--, s1++, s2++) + ; /* no loop body! */ + return ((s1len == 0) && (*s2 == 0)) ? XML_TRUE : XML_FALSE; +# endif +#else + return (strncmp(s1, s2, s1len) == 0 && s2[s1len] == '\0') ? XML_TRUE + : XML_FALSE; +#endif +} + +XML_NONTESTING_STATIC size_t +keylen(KEY s) { + return xcslen(s); +} + +static void +copy_salt_to_sipkey(XML_Parser parser, struct sipkey *key) { + const XML_Parser rootParser = getRootParserOf(parser, NULL); + assert(! rootParser->m_parentParser); + + *key = rootParser->m_hash_secret_salt_128; +} + +static unsigned long +hash(XML_Parser parser, KEY s, size_t keyLen) { + struct siphash state; + struct sipkey key; + (void)sip24_valid; + copy_salt_to_sipkey(parser, &key); + sip24_init(&state, &key); + sip24_update(&state, s, keyLen * sizeof(XML_Char)); + return (unsigned long)sip24_final(&state); +} + +// Function `lookupWithLength` can be used to either… +// +// a) check whether an element with key `name` exists in the given hash table +// (read-only mode where `createSize == 0`) or +// +// b) check whether an element with key `name` exists in the given hash table +// *and* insert it if missing (i.e. read-write mode where `createSize != 0`. +// +// When inserting, a block of `createSize` number of bytes will be allocated +// and set to zero, and the resulting block of memory will be considered +// to start with a `NAMED` structure, and `->name = name;` is performed. +// The fact that all other bytes in the structure are initially zero can +// be used to tell cases "existed and found" and "newly inserted" apart +// with the structure returned. +// +// NOTE: Read-only lookup does not need zero-terminated keys but +// read-write mode does, because keys can be re-hashed later and the +// hash table does not store key length information. +// +XML_NONTESTING_STATIC NAMED * +lookupWithLength(XML_Parser parser, HASH_TABLE *table, KEY name, size_t nameLen, + size_t createSize) { + size_t i; + if (table->size == 0) { + size_t tsize; + if (! createSize) + return NULL; + table->power = INIT_POWER; + /* table->size is a power of 2 */ + table->size = (size_t)1 << INIT_POWER; + tsize = table->size * sizeof(NAMED *); + table->v = MALLOC(table->parser, tsize); + if (! table->v) { + table->size = 0; + return NULL; + } + memset(table->v, 0, tsize); + i = hash(parser, name, nameLen) & ((unsigned long)table->size - 1); + } else { + unsigned long h = hash(parser, name, nameLen); + unsigned long mask = (unsigned long)table->size - 1; + unsigned char step = 0; + i = h & mask; + while (table->v[i]) { + if (keyeq(name, nameLen, table->v[i]->name)) + return table->v[i]; + if (! step) + step = PROBE_STEP(h, mask, table->power); + i < step ? (i += table->size - step) : (i -= step); + } + if (! createSize) + return NULL; + + /* check for overflow (table is half full) */ + if (table->used >> (table->power - 1)) { + unsigned char newPower = table->power + 1; + + /* Detect and prevent invalid shift */ + if (newPower >= sizeof(unsigned long) * 8 /* bits per byte */) { + return NULL; + } + + size_t newSize = (size_t)1 << newPower; + unsigned long newMask = (unsigned long)newSize - 1; + + /* Detect and prevent integer overflow */ + if (newSize > SIZE_MAX / sizeof(NAMED *)) { + return NULL; + } + + size_t tsize = newSize * sizeof(NAMED *); + NAMED **newV = MALLOC(table->parser, tsize); + if (! newV) + return NULL; + memset(newV, 0, tsize); + for (i = 0; i < table->size; i++) + if (table->v[i]) { + KEY const key = table->v[i]->name; + unsigned long newHash = hash(parser, key, keylen(key)); + size_t j = newHash & newMask; + step = 0; + while (newV[j]) { + if (! step) + step = PROBE_STEP(newHash, newMask, newPower); + j < step ? (j += newSize - step) : (j -= step); + } + newV[j] = table->v[i]; + } + FREE(table->parser, table->v); + table->v = newV; + table->power = newPower; + table->size = newSize; + i = h & newMask; + step = 0; + while (table->v[i]) { + if (! step) + step = PROBE_STEP(h, newMask, newPower); + i < step ? (i += newSize - step) : (i -= step); + } + } + } + assert(createSize >= sizeof(NAMED)); + table->v[i] = MALLOC(table->parser, createSize); + if (! table->v[i]) + return NULL; + memset(table->v[i], 0, createSize); + table->v[i]->name = name; // NOTE: This requires and assumes zero termination! + (table->used)++; + return table->v[i]; +} + +// Function `lookup` can be used to either… +// +// a) check whether an element with key `name` exists in the given hash table +// (read-only mode where `createSize == 0`) or +// +// b) check whether an element with key `name` exists in the given hash table +// *and* insert it if missing (i.e. read-write mode where `createSize != 0`. +// +// When inserting, a block of `createSize` number of bytes will be allocated +// and set to zero, and the resulting block of memory will be considered +// to start with a `NAMED` structure, and `->name = name;` is performed. +// The fact that all other bytes in the structure are initially zero can +// be used to tell cases "existed and found" and "newly inserted" apart +// with the structure returned. +// +XML_NONTESTING_STATIC NAMED * +lookup(XML_Parser parser, HASH_TABLE *table, KEY name, size_t createSize) { + return lookupWithLength(parser, table, name, keylen(name), createSize); +} + +XML_NONTESTING_STATIC void +hashTableClear(HASH_TABLE *table) { + size_t i; + for (i = 0; i < table->size; i++) { + FREE(table->parser, table->v[i]); + table->v[i] = NULL; + } + table->used = 0; +} + +XML_NONTESTING_STATIC void +hashTableDestroy(HASH_TABLE *table) { + size_t i; + for (i = 0; i < table->size; i++) + FREE(table->parser, table->v[i]); + FREE(table->parser, table->v); +} + +XML_NONTESTING_STATIC void +hashTableInit(HASH_TABLE *p, XML_Parser parser) { + p->power = 0; + p->size = 0; + p->used = 0; + p->v = NULL; + p->parser = parser; +} + +XML_NONTESTING_STATIC void +hashTableIterInit(HASH_TABLE_ITER *iter, const HASH_TABLE *table) { + iter->p = table->v; + iter->end = iter->p ? iter->p + table->size : NULL; +} + +XML_NONTESTING_STATIC NAMED * +hashTableIterNext(HASH_TABLE_ITER *iter) { + while (iter->p != iter->end) { + NAMED *tem = *(iter->p)++; + if (tem) + return tem; + } + return NULL; +} + +static void +poolInit(STRING_POOL *pool, XML_Parser parser) { + pool->blocks = NULL; + pool->freeBlocks = NULL; + pool->start = NULL; + pool->ptr = NULL; + pool->end = NULL; + pool->parser = parser; +} + +static void +poolClear(STRING_POOL *pool) { + if (! pool->freeBlocks) + pool->freeBlocks = pool->blocks; + else { + BLOCK *p = pool->blocks; + while (p) { + BLOCK *tem = p->next; + p->next = pool->freeBlocks; + pool->freeBlocks = p; + p = tem; + } + } + pool->blocks = NULL; + pool->start = NULL; + pool->ptr = NULL; + pool->end = NULL; +} + +static void +poolDestroy(STRING_POOL *pool) { + BLOCK *p = pool->blocks; + while (p) { + BLOCK *tem = p->next; + FREE(pool->parser, p); + p = tem; + } + p = pool->freeBlocks; + while (p) { + BLOCK *tem = p->next; + FREE(pool->parser, p); + p = tem; + } +} + +static XML_Char * +poolAppend(STRING_POOL *pool, const ENCODING *enc, const char *ptr, + const char *end) { + if (! pool->ptr && ! poolGrow(pool)) + return NULL; + for (;;) { + const enum XML_Convert_Result convert_res = XmlConvert( + enc, &ptr, end, (ICHAR **)&(pool->ptr), (const ICHAR *)pool->end); + if ((convert_res == XML_CONVERT_COMPLETED) + || (convert_res == XML_CONVERT_INPUT_INCOMPLETE)) + break; + if (! poolGrow(pool)) + return NULL; + } + return pool->start; +} + +static const XML_Char * +poolCopyString(STRING_POOL *pool, const XML_Char *s) { + if (! poolAppendChars(pool, s, xcslen(s) + /*null terminator*/ 1)) + return NULL; + s = pool->start; + poolFinish(pool); + return s; +} + +// A version of `poolCopyString` that does not call `poolFinish` +// and reverts any partial advancement upon failure. +static const XML_Char * +poolCopyStringNoFinish(STRING_POOL *pool, const XML_Char *s) { + const XML_Char *const original = s; + do { + if (! poolAppendChar(pool, *s)) { + // Revert any previously successful advancement + const ptrdiff_t advancedBy = s - original; + if (advancedBy > 0) + pool->ptr -= advancedBy; + return NULL; + } + } while (*s++); + return pool->start; +} + +static const XML_Char * +poolCopyStringN(STRING_POOL *pool, const XML_Char *s, int n) { + if (! pool->ptr && ! poolGrow(pool)) { + /* The following line is unreachable given the current usage of + * poolCopyStringN(). Currently it is called from exactly one + * place to copy the text of a simple general entity. By that + * point, the name of the entity is already stored in the pool, so + * pool->ptr cannot be NULL. + * + * If poolCopyStringN() is used elsewhere as it well might be, + * this line may well become executable again. Regardless, this + * sort of check shouldn't be removed lightly, so we just exclude + * it from the coverage statistics. + */ + return NULL; /* LCOV_EXCL_LINE */ + } + if (n > 0 && ! poolAppendChars(pool, s, n)) + return NULL; + s = pool->start; + poolFinish(pool); + return s; +} + +static const XML_Char * +poolAppendString(STRING_POOL *pool, const XML_Char *s) { + if (! poolAppendChars(pool, s, xcslen(s))) + return NULL; + return pool->start; +} + +static XML_Char * +poolStoreString(STRING_POOL *pool, const ENCODING *enc, const char *ptr, + const char *end) { + if (! poolAppend(pool, enc, ptr, end)) + return NULL; + if (! poolAppendChar(pool, 0)) + return NULL; + return pool->start; +} + +static size_t +poolBytesToAllocateFor(int blockSize) { + /* Unprotected math would be: + ** return offsetof(BLOCK, s) + blockSize * sizeof(XML_Char); + ** + ** Detect overflow, avoiding _signed_ overflow undefined behavior + ** For a + b * c we check b * c in isolation first, so that addition of a + ** on top has no chance of making us accept a small non-negative number + */ + const size_t stretch = sizeof(XML_Char); /* can be 4 bytes */ + + if (blockSize <= 0) + return 0; + + if (blockSize > (int)(INT_MAX / stretch)) + return 0; + + { + const int stretchedBlockSize = blockSize * (int)stretch; + const int bytesToAllocate + = (int)(offsetof(BLOCK, s) + (unsigned)stretchedBlockSize); + if (bytesToAllocate < 0) + return 0; + + return (size_t)bytesToAllocate; + } +} + +static XML_Bool +poolGrow(STRING_POOL *pool) { + if (pool->freeBlocks) { + if (pool->start == NULL) { + pool->blocks = pool->freeBlocks; + pool->freeBlocks = pool->freeBlocks->next; + pool->blocks->next = NULL; + pool->start = pool->blocks->s; + pool->end = pool->start + pool->blocks->size; + pool->ptr = pool->start; + return XML_TRUE; + } + if (pool->end - pool->start < pool->freeBlocks->size) { + BLOCK *tem = pool->freeBlocks->next; + pool->freeBlocks->next = pool->blocks; + pool->blocks = pool->freeBlocks; + pool->freeBlocks = tem; + memcpy(pool->blocks->s, pool->start, + (pool->end - pool->start) * sizeof(XML_Char)); + pool->ptr = pool->blocks->s + EXPAT_SAFE_PTR_DIFF(pool->ptr, pool->start); + pool->start = pool->blocks->s; + pool->end = pool->start + pool->blocks->size; + return XML_TRUE; + } + } + if (pool->blocks && pool->start == pool->blocks->s) { + BLOCK *temp; + int blockSize = (int)((unsigned)(pool->end - pool->start) * 2U); + size_t bytesToAllocate; + + /* NOTE: Needs to be calculated prior to calling `realloc` + to avoid dangling pointers: */ + const ptrdiff_t offsetInsideBlock + = EXPAT_SAFE_PTR_DIFF(pool->ptr, pool->start); + + if (blockSize < 0) { + /* This condition traps a situation where either more than + * INT_MAX/2 bytes have already been allocated. This isn't + * readily testable, since it is unlikely that an average + * machine will have that much memory, so we exclude it from the + * coverage statistics. + */ + return XML_FALSE; /* LCOV_EXCL_LINE */ + } + + bytesToAllocate = poolBytesToAllocateFor(blockSize); + if (bytesToAllocate == 0) + return XML_FALSE; + + temp = REALLOC(pool->parser, pool->blocks, bytesToAllocate); + if (temp == NULL) + return XML_FALSE; + pool->blocks = temp; + pool->blocks->size = blockSize; + pool->ptr = pool->blocks->s + offsetInsideBlock; + pool->start = pool->blocks->s; + pool->end = pool->start + blockSize; + } else { + BLOCK *tem; + int blockSize = (int)(pool->end - pool->start); + size_t bytesToAllocate; + + if (blockSize < 0) { + /* This condition traps a situation where either more than + * INT_MAX bytes have already been allocated (which is prevented + * by various pieces of program logic, not least this one, never + * mind the unlikelihood of actually having that much memory) or + * the pool control fields have been corrupted (which could + * conceivably happen in an extremely buggy user handler + * function). Either way it isn't readily testable, so we + * exclude it from the coverage statistics. + */ + return XML_FALSE; /* LCOV_EXCL_LINE */ + } + + if (blockSize < INIT_BLOCK_SIZE) + blockSize = INIT_BLOCK_SIZE; + else { + /* Detect overflow, avoiding _signed_ overflow undefined behavior */ + if ((int)((unsigned)blockSize * 2U) < 0) { + return XML_FALSE; + } + blockSize *= 2; + } + + bytesToAllocate = poolBytesToAllocateFor(blockSize); + if (bytesToAllocate == 0) + return XML_FALSE; + + tem = MALLOC(pool->parser, bytesToAllocate); + if (! tem) + return XML_FALSE; + tem->size = blockSize; + tem->next = pool->blocks; + pool->blocks = tem; + if (pool->ptr != pool->start) + memcpy(tem->s, pool->start, + EXPAT_SAFE_PTR_DIFF(pool->ptr, pool->start) * sizeof(XML_Char)); + pool->ptr = tem->s + EXPAT_SAFE_PTR_DIFF(pool->ptr, pool->start); + pool->start = tem->s; + pool->end = tem->s + blockSize; + } + return XML_TRUE; +} + +static bool +poolGrowUntil(STRING_POOL *pool, size_t needed) { + for (;;) { + const size_t available = pool->end - pool->ptr; + if (available >= needed) { + return true; + } + if (! poolGrow(pool)) { + return false; + } + } +} + +static int +nextScaffoldPart(XML_Parser parser) { + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + CONTENT_SCAFFOLD *me; + int next; + + if (! dtd->scaffIndex) { + /* Detect and prevent integer overflow. */ + if (parser->m_groupSize > SIZE_MAX / sizeof(int)) { + return -1; + } + dtd->scaffIndex = MALLOC(parser, parser->m_groupSize * sizeof(int)); + if (! dtd->scaffIndex) + return -1; + dtd->scaffIndexSize = parser->m_groupSize; + dtd->scaffIndex[0] = 0; + } + + // Will casting to int be safe further down? + if (dtd->scaffCount > INT_MAX) { + return -1; + } + + if (dtd->scaffCount >= dtd->scaffSize) { + CONTENT_SCAFFOLD *temp; + if (dtd->scaffold) { + /* Detect and prevent integer overflow */ + if (dtd->scaffSize > UINT_MAX / 2u) { + return -1; + } + /* Detect and prevent integer overflow. + * The preprocessor guard addresses the "always false" warning + * from -Wtype-limits on platforms where + * sizeof(unsigned int) < sizeof(size_t), e.g. on x86_64. */ +#if UINT_MAX >= SIZE_MAX + if (dtd->scaffSize > SIZE_MAX / 2u / sizeof(CONTENT_SCAFFOLD)) { + return -1; + } +#endif + + temp = REALLOC(parser, dtd->scaffold, + dtd->scaffSize * 2 * sizeof(CONTENT_SCAFFOLD)); + if (temp == NULL) + return -1; + dtd->scaffSize *= 2; + } else { + temp = MALLOC(parser, INIT_SCAFFOLD_ELEMENTS * sizeof(CONTENT_SCAFFOLD)); + if (temp == NULL) + return -1; + dtd->scaffSize = INIT_SCAFFOLD_ELEMENTS; + } + dtd->scaffold = temp; + } + next = (int)dtd->scaffCount++; + me = &dtd->scaffold[next]; + if (dtd->scaffLevel) { + CONTENT_SCAFFOLD *parent + = &dtd->scaffold[dtd->scaffIndex[dtd->scaffLevel - 1]]; + if (parent->lastchild) { + dtd->scaffold[parent->lastchild].nextsib = next; + } + if (! parent->childcnt) + parent->firstchild = next; + parent->lastchild = next; + parent->childcnt++; + } + me->firstchild = me->lastchild = me->childcnt = me->nextsib = 0; + return next; +} + +static XML_Content * +build_model(XML_Parser parser) { + /* Function build_model transforms the existing parser->m_dtd->scaffold + * array of CONTENT_SCAFFOLD tree nodes into a new array of + * XML_Content tree nodes followed by a gapless list of zero-terminated + * strings. */ + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + XML_Content *ret; + XML_Char *str; /* the current string writing location */ + + /* Detect and prevent integer overflow. + * The preprocessor guard addresses the "always false" warning + * from -Wtype-limits on platforms where + * sizeof(unsigned int) < sizeof(size_t), e.g. on x86_64. */ +#if UINT_MAX >= SIZE_MAX + if (dtd->scaffCount > SIZE_MAX / sizeof(XML_Content)) { + return NULL; + } + if (dtd->contentStringLen > SIZE_MAX / sizeof(XML_Char)) { + return NULL; + } +#endif + if (dtd->scaffCount * sizeof(XML_Content) + > SIZE_MAX - dtd->contentStringLen * sizeof(XML_Char)) { + return NULL; + } + + const size_t allocsize = (dtd->scaffCount * sizeof(XML_Content) + + (dtd->contentStringLen * sizeof(XML_Char))); + + // NOTE: We are avoiding MALLOC(..) here to so that + // applications that are not using XML_FreeContentModel but plain + // free(..) or .free_fcn() to free the content model's memory are safe. + ret = parser->m_mem.malloc_fcn(allocsize); + if (! ret) + return NULL; + + /* What follows is an iterative implementation (of what was previously done + * recursively in a dedicated function called "build_node". The old recursive + * build_node could be forced into stack exhaustion from input as small as a + * few megabyte, and so that was a security issue. Hence, a function call + * stack is avoided now by resolving recursion.) + * + * The iterative approach works as follows: + * + * - We have two writing pointers, both walking up the result array; one does + * the work, the other creates "jobs" for its colleague to do, and leads + * the way: + * + * - The faster one, pointer jobDest, always leads and writes "what job + * to do" by the other, once they reach that place in the + * array: leader "jobDest" stores the source node array index (relative + * to array dtd->scaffold) in field "numchildren". + * + * - The slower one, pointer dest, looks at the value stored in the + * "numchildren" field (which actually holds a source node array index + * at that time) and puts the real data from dtd->scaffold in. + * + * - Before the loop starts, jobDest writes source array index 0 + * (where the root node is located) so that dest will have something to do + * when it starts operation. + * + * - Whenever nodes with children are encountered, jobDest appends + * them as new jobs, in order. As a result, tree node siblings are + * adjacent in the resulting array, for example: + * + * [0] root, has two children + * [1] first child of 0, has three children + * [3] first child of 1, does not have children + * [4] second child of 1, does not have children + * [5] third child of 1, does not have children + * [2] second child of 0, does not have children + * + * Or (the same data) presented in flat array view: + * + * [0] root, has two children + * + * [1] first child of 0, has three children + * [2] second child of 0, does not have children + * + * [3] first child of 1, does not have children + * [4] second child of 1, does not have children + * [5] third child of 1, does not have children + * + * - The algorithm repeats until all target array indices have been processed. + */ + XML_Content *dest = ret; /* tree node writing location, moves upwards */ + XML_Content *const destLimit = &ret[dtd->scaffCount]; + XML_Content *jobDest = ret; /* next free writing location in target array */ + str = (XML_Char *)&ret[dtd->scaffCount]; + + /* Add the starting job, the root node (index 0) of the source tree */ + (jobDest++)->numchildren = 0; + + for (; dest < destLimit; dest++) { + /* Retrieve source tree array index from job storage */ + const int src_node = (int)dest->numchildren; + + /* Convert item */ + dest->type = dtd->scaffold[src_node].type; + dest->quant = dtd->scaffold[src_node].quant; + if (dest->type == XML_CTYPE_NAME) { + const XML_Char *src; + dest->name = str; + src = dtd->scaffold[src_node].name; + + const size_t nameLen = xcslen(src) + /* null terminator*/ 1; + + // Detect and prevent integer overflow + if (nameLen > SIZE_MAX / sizeof(XML_Char)) { + // NOTE: We are avoiding FREE(..) here because the model + // is not being allocated with MALLOC(..) but with plain + // .malloc_fcn(..). + parser->m_mem.free_fcn(ret); + return NULL; + } + + memcpy(str, src, nameLen * sizeof(XML_Char)); + str += nameLen; + + dest->numchildren = 0; + dest->children = NULL; + } else { + unsigned int i; + int cn; + dest->name = NULL; + dest->numchildren = dtd->scaffold[src_node].childcnt; + dest->children = jobDest; + + /* Append scaffold indices of children to array */ + for (i = 0, cn = dtd->scaffold[src_node].firstchild; + i < dest->numchildren; i++, cn = dtd->scaffold[cn].nextsib) + (jobDest++)->numchildren = (unsigned int)cn; + } + } + + return ret; +} + +static ELEMENT_TYPE * +getElementType(XML_Parser parser, const ENCODING *enc, const char *ptr, + const char *end) { + DTD *const dtd = parser->m_dtd; /* save one level of indirection */ + const XML_Char *name = poolStoreString(&dtd->pool, enc, ptr, end); + ELEMENT_TYPE *ret; + + if (! name) + return NULL; + ret = (ELEMENT_TYPE *)lookup(parser, &dtd->elementTypes, name, + sizeof(ELEMENT_TYPE)); + if (! ret) + return NULL; + if (! ret->defaultAttForName.parser) + hashTableInit(&(ret->defaultAttForName), getRootParserOf(parser, NULL)); + if (ret->name != name) + poolDiscard(&dtd->pool); + else { + poolFinish(&dtd->pool); + if (! setElementTypePrefix(parser, ret)) + return NULL; + } + return ret; +} + +static XML_Char * +copyString(const XML_Char *s, XML_Parser parser) { + /* First determine how long the string is */ + const size_t charsRequired = xcslen(s) + /*null terminator*/ 1; + + /* Detect and prevent integer overflow */ + if (charsRequired > SIZE_MAX / sizeof(XML_Char)) + return NULL; + + const size_t bytesRequired = charsRequired * sizeof(XML_Char); + + /* Now allocate space for the copy */ + XML_Char *const result = MALLOC(parser, bytesRequired); + + if (result == NULL) + return NULL; + + /* Copy the original into place */ + memcpy(result, s, bytesRequired); + + return result; +} + +#if XML_GE == 1 + +static float +accountingGetCurrentAmplification(XML_Parser rootParser) { + // 1.........1.........12 => 22 + const size_t lenOfShortestInclude = sizeof("") - 1; + const XmlBigCount countBytesOutput + = rootParser->m_accounting.countBytesDirect + + rootParser->m_accounting.countBytesIndirect; + const float amplificationFactor + = rootParser->m_accounting.countBytesDirect + ? ((float)countBytesOutput + / (float)(rootParser->m_accounting.countBytesDirect)) + : ((float)(lenOfShortestInclude + + rootParser->m_accounting.countBytesIndirect) + / (float)lenOfShortestInclude); + assert(! rootParser->m_parentParser); + return amplificationFactor; +} + +static void +accountingReportStats(XML_Parser originParser, const char *epilog) { + const XML_Parser rootParser = getRootParserOf(originParser, NULL); + assert(! rootParser->m_parentParser); + + if (rootParser->m_accounting.debugLevel == 0u) { + return; + } + + const float amplificationFactor + = accountingGetCurrentAmplification(rootParser); + fprintf(stderr, + "expat: Accounting(%p): Direct " EXPAT_FMT_ULL( + "10") ", indirect " EXPAT_FMT_ULL("10") ", amplification %8.2f%s", + (void *)rootParser, rootParser->m_accounting.countBytesDirect, + rootParser->m_accounting.countBytesIndirect, + (double)amplificationFactor, epilog); +} + +static void +accountingOnAbort(XML_Parser originParser) { + accountingReportStats(originParser, " ABORTING\n"); +} + +static void +accountingReportDiff(XML_Parser rootParser, + unsigned int levelsAwayFromRootParser, const char *before, + const char *after, ptrdiff_t bytesMore, int source_line, + enum XML_Account account) { + assert(! rootParser->m_parentParser); + + fprintf(stderr, + " (+" EXPAT_FMT_PTRDIFF_T("6") " bytes %s|%u, xmlparse.c:%d) %*s\"", + bytesMore, (account == XML_ACCOUNT_DIRECT) ? "DIR" : "EXP", + levelsAwayFromRootParser, source_line, 10, ""); + + const char ellipis[] = "[..]"; + const size_t ellipsisLength = sizeof(ellipis) /* because compile-time */ - 1; + const unsigned int contextLength = 10; + + /* Note: Performance is of no concern here */ + const char *walker = before; + if ((rootParser->m_accounting.debugLevel >= 3u) + || (after - before) + <= (ptrdiff_t)(contextLength + ellipsisLength + contextLength)) { + for (; walker < after; walker++) { + fprintf(stderr, "%s", unsignedCharToPrintable(walker[0])); + } + } else { + for (; walker < before + contextLength; walker++) { + fprintf(stderr, "%s", unsignedCharToPrintable(walker[0])); + } + fprintf(stderr, ellipis); + walker = after - contextLength; + for (; walker < after; walker++) { + fprintf(stderr, "%s", unsignedCharToPrintable(walker[0])); + } + } + fprintf(stderr, "\"\n"); +} + +static XML_Bool +accountingDiffTolerated(XML_Parser originParser, int tok, const char *before, + const char *after, int source_line, + enum XML_Account account) { + /* Note: We need to check the token type *first* to be sure that + * we can even access variable , safely. + * E.g. for XML_TOK_NONE may hold an invalid pointer. */ + switch (tok) { + case XML_TOK_INVALID: + case XML_TOK_PARTIAL: + case XML_TOK_PARTIAL_CHAR: + case XML_TOK_NONE: + return XML_TRUE; + } + + if (account == XML_ACCOUNT_NONE) + return XML_TRUE; /* because these bytes have been accounted for, already */ + + unsigned int levelsAwayFromRootParser; + const XML_Parser rootParser + = getRootParserOf(originParser, &levelsAwayFromRootParser); + assert(! rootParser->m_parentParser); + + const int isDirect + = (account == XML_ACCOUNT_DIRECT) && (originParser == rootParser); + const ptrdiff_t bytesMore = after - before; + + XmlBigCount *const additionTarget + = isDirect ? &rootParser->m_accounting.countBytesDirect + : &rootParser->m_accounting.countBytesIndirect; + + /* Detect and avoid integer overflow */ + if (*additionTarget > (XmlBigCount)(-1) - (XmlBigCount)bytesMore) + return XML_FALSE; + *additionTarget += bytesMore; + + const XmlBigCount countBytesOutput + = rootParser->m_accounting.countBytesDirect + + rootParser->m_accounting.countBytesIndirect; + const float amplificationFactor + = accountingGetCurrentAmplification(rootParser); + const XML_Bool tolerated + = (countBytesOutput < rootParser->m_accounting.activationThresholdBytes) + || (amplificationFactor + <= rootParser->m_accounting.maximumAmplificationFactor); + + if (rootParser->m_accounting.debugLevel >= 2u) { + accountingReportStats(rootParser, ""); + accountingReportDiff(rootParser, levelsAwayFromRootParser, before, after, + bytesMore, source_line, account); + } + + return tolerated; +} + +unsigned long long +testingAccountingGetCountBytesDirect(XML_Parser parser) { + if (! parser) + return 0; + return parser->m_accounting.countBytesDirect; +} + +unsigned long long +testingAccountingGetCountBytesIndirect(XML_Parser parser) { + if (! parser) + return 0; + return parser->m_accounting.countBytesIndirect; +} + +static void +entityTrackingReportStats(XML_Parser rootParser, ENTITY *entity, + const char *action, int sourceLine) { + assert(! rootParser->m_parentParser); + if (rootParser->m_entity_stats.debugLevel == 0u) + return; + +# if defined(XML_UNICODE) + const char *const entityName = "[..]"; +# else + const char *const entityName = entity->name; +# endif + + const bool limitingWanted = rootParser->m_entity_stats.debugLevel < 2; + const int maxLimitedDepth = 10; // somewhat arbitrary + const int candidateIndentDepth + = (int)rootParser->m_entity_stats.currentDepth - 1; + const bool limitingNeeded + = limitingWanted && (candidateIndentDepth > maxLimitedDepth); + const char *const ellipisOrEmpty = limitingNeeded ? " [..] " : ""; + const int indentDepth + = limitingNeeded ? (maxLimitedDepth - /* make space for ellipis */ 2) + : candidateIndentDepth; + + fprintf( + stderr, + "expat: Entities(%p): Count %9u, depth %2u/%2u %*s%s%s%s; %s length %d (xmlparse.c:%d)\n", + (void *)rootParser, rootParser->m_entity_stats.countEverOpened, + rootParser->m_entity_stats.currentDepth, + rootParser->m_entity_stats.maximumDepthSeen, indentDepth * 2, "", + ellipisOrEmpty, entity->is_param ? "%" : "&", entityName, action, + entity->textLen, sourceLine); +} + +static void +entityTrackingOnOpen(XML_Parser originParser, ENTITY *entity, int sourceLine) { + const XML_Parser rootParser = getRootParserOf(originParser, NULL); + assert(! rootParser->m_parentParser); + + rootParser->m_entity_stats.countEverOpened++; + rootParser->m_entity_stats.currentDepth++; + if (rootParser->m_entity_stats.currentDepth + > rootParser->m_entity_stats.maximumDepthSeen) { + rootParser->m_entity_stats.maximumDepthSeen++; + } + + entityTrackingReportStats(rootParser, entity, "OPEN ", sourceLine); +} + +static void +entityTrackingOnClose(XML_Parser originParser, ENTITY *entity, int sourceLine) { + const XML_Parser rootParser = getRootParserOf(originParser, NULL); + assert(! rootParser->m_parentParser); + + entityTrackingReportStats(rootParser, entity, "CLOSE", sourceLine); + rootParser->m_entity_stats.currentDepth--; +} + +#endif /* XML_GE == 1 */ + +static XML_Parser +getRootParserOf(XML_Parser parser, unsigned int *outLevelDiff) { + XML_Parser rootParser = parser; + unsigned int stepsTakenUpwards = 0; + while (rootParser->m_parentParser) { + rootParser = rootParser->m_parentParser; + stepsTakenUpwards++; + } + assert(! rootParser->m_parentParser); + if (outLevelDiff != NULL) { + *outLevelDiff = stepsTakenUpwards; + } + return rootParser; +} + +#if XML_GE == 1 + +const char * +unsignedCharToPrintable(unsigned char c) { + switch (c) { + case 0: + return "\\0"; + case 1: + return "\\x1"; + case 2: + return "\\x2"; + case 3: + return "\\x3"; + case 4: + return "\\x4"; + case 5: + return "\\x5"; + case 6: + return "\\x6"; + case 7: + return "\\x7"; + case 8: + return "\\x8"; + case 9: + return "\\t"; + case 10: + return "\\n"; + case 11: + return "\\xB"; + case 12: + return "\\xC"; + case 13: + return "\\r"; + case 14: + return "\\xE"; + case 15: + return "\\xF"; + case 16: + return "\\x10"; + case 17: + return "\\x11"; + case 18: + return "\\x12"; + case 19: + return "\\x13"; + case 20: + return "\\x14"; + case 21: + return "\\x15"; + case 22: + return "\\x16"; + case 23: + return "\\x17"; + case 24: + return "\\x18"; + case 25: + return "\\x19"; + case 26: + return "\\x1A"; + case 27: + return "\\x1B"; + case 28: + return "\\x1C"; + case 29: + return "\\x1D"; + case 30: + return "\\x1E"; + case 31: + return "\\x1F"; + case 32: + return " "; + case 33: + return "!"; + case 34: + return "\\\""; + case 35: + return "#"; + case 36: + return "$"; + case 37: + return "%"; + case 38: + return "&"; + case 39: + return "'"; + case 40: + return "("; + case 41: + return ")"; + case 42: + return "*"; + case 43: + return "+"; + case 44: + return ","; + case 45: + return "-"; + case 46: + return "."; + case 47: + return "/"; + case 48: + return "0"; + case 49: + return "1"; + case 50: + return "2"; + case 51: + return "3"; + case 52: + return "4"; + case 53: + return "5"; + case 54: + return "6"; + case 55: + return "7"; + case 56: + return "8"; + case 57: + return "9"; + case 58: + return ":"; + case 59: + return ";"; + case 60: + return "<"; + case 61: + return "="; + case 62: + return ">"; + case 63: + return "?"; + case 64: + return "@"; + case 65: + return "A"; + case 66: + return "B"; + case 67: + return "C"; + case 68: + return "D"; + case 69: + return "E"; + case 70: + return "F"; + case 71: + return "G"; + case 72: + return "H"; + case 73: + return "I"; + case 74: + return "J"; + case 75: + return "K"; + case 76: + return "L"; + case 77: + return "M"; + case 78: + return "N"; + case 79: + return "O"; + case 80: + return "P"; + case 81: + return "Q"; + case 82: + return "R"; + case 83: + return "S"; + case 84: + return "T"; + case 85: + return "U"; + case 86: + return "V"; + case 87: + return "W"; + case 88: + return "X"; + case 89: + return "Y"; + case 90: + return "Z"; + case 91: + return "["; + case 92: + return "\\\\"; + case 93: + return "]"; + case 94: + return "^"; + case 95: + return "_"; + case 96: + return "`"; + case 97: + return "a"; + case 98: + return "b"; + case 99: + return "c"; + case 100: + return "d"; + case 101: + return "e"; + case 102: + return "f"; + case 103: + return "g"; + case 104: + return "h"; + case 105: + return "i"; + case 106: + return "j"; + case 107: + return "k"; + case 108: + return "l"; + case 109: + return "m"; + case 110: + return "n"; + case 111: + return "o"; + case 112: + return "p"; + case 113: + return "q"; + case 114: + return "r"; + case 115: + return "s"; + case 116: + return "t"; + case 117: + return "u"; + case 118: + return "v"; + case 119: + return "w"; + case 120: + return "x"; + case 121: + return "y"; + case 122: + return "z"; + case 123: + return "{"; + case 124: + return "|"; + case 125: + return "}"; + case 126: + return "~"; + case 127: + return "\\x7F"; + case 128: + return "\\x80"; + case 129: + return "\\x81"; + case 130: + return "\\x82"; + case 131: + return "\\x83"; + case 132: + return "\\x84"; + case 133: + return "\\x85"; + case 134: + return "\\x86"; + case 135: + return "\\x87"; + case 136: + return "\\x88"; + case 137: + return "\\x89"; + case 138: + return "\\x8A"; + case 139: + return "\\x8B"; + case 140: + return "\\x8C"; + case 141: + return "\\x8D"; + case 142: + return "\\x8E"; + case 143: + return "\\x8F"; + case 144: + return "\\x90"; + case 145: + return "\\x91"; + case 146: + return "\\x92"; + case 147: + return "\\x93"; + case 148: + return "\\x94"; + case 149: + return "\\x95"; + case 150: + return "\\x96"; + case 151: + return "\\x97"; + case 152: + return "\\x98"; + case 153: + return "\\x99"; + case 154: + return "\\x9A"; + case 155: + return "\\x9B"; + case 156: + return "\\x9C"; + case 157: + return "\\x9D"; + case 158: + return "\\x9E"; + case 159: + return "\\x9F"; + case 160: + return "\\xA0"; + case 161: + return "\\xA1"; + case 162: + return "\\xA2"; + case 163: + return "\\xA3"; + case 164: + return "\\xA4"; + case 165: + return "\\xA5"; + case 166: + return "\\xA6"; + case 167: + return "\\xA7"; + case 168: + return "\\xA8"; + case 169: + return "\\xA9"; + case 170: + return "\\xAA"; + case 171: + return "\\xAB"; + case 172: + return "\\xAC"; + case 173: + return "\\xAD"; + case 174: + return "\\xAE"; + case 175: + return "\\xAF"; + case 176: + return "\\xB0"; + case 177: + return "\\xB1"; + case 178: + return "\\xB2"; + case 179: + return "\\xB3"; + case 180: + return "\\xB4"; + case 181: + return "\\xB5"; + case 182: + return "\\xB6"; + case 183: + return "\\xB7"; + case 184: + return "\\xB8"; + case 185: + return "\\xB9"; + case 186: + return "\\xBA"; + case 187: + return "\\xBB"; + case 188: + return "\\xBC"; + case 189: + return "\\xBD"; + case 190: + return "\\xBE"; + case 191: + return "\\xBF"; + case 192: + return "\\xC0"; + case 193: + return "\\xC1"; + case 194: + return "\\xC2"; + case 195: + return "\\xC3"; + case 196: + return "\\xC4"; + case 197: + return "\\xC5"; + case 198: + return "\\xC6"; + case 199: + return "\\xC7"; + case 200: + return "\\xC8"; + case 201: + return "\\xC9"; + case 202: + return "\\xCA"; + case 203: + return "\\xCB"; + case 204: + return "\\xCC"; + case 205: + return "\\xCD"; + case 206: + return "\\xCE"; + case 207: + return "\\xCF"; + case 208: + return "\\xD0"; + case 209: + return "\\xD1"; + case 210: + return "\\xD2"; + case 211: + return "\\xD3"; + case 212: + return "\\xD4"; + case 213: + return "\\xD5"; + case 214: + return "\\xD6"; + case 215: + return "\\xD7"; + case 216: + return "\\xD8"; + case 217: + return "\\xD9"; + case 218: + return "\\xDA"; + case 219: + return "\\xDB"; + case 220: + return "\\xDC"; + case 221: + return "\\xDD"; + case 222: + return "\\xDE"; + case 223: + return "\\xDF"; + case 224: + return "\\xE0"; + case 225: + return "\\xE1"; + case 226: + return "\\xE2"; + case 227: + return "\\xE3"; + case 228: + return "\\xE4"; + case 229: + return "\\xE5"; + case 230: + return "\\xE6"; + case 231: + return "\\xE7"; + case 232: + return "\\xE8"; + case 233: + return "\\xE9"; + case 234: + return "\\xEA"; + case 235: + return "\\xEB"; + case 236: + return "\\xEC"; + case 237: + return "\\xED"; + case 238: + return "\\xEE"; + case 239: + return "\\xEF"; + case 240: + return "\\xF0"; + case 241: + return "\\xF1"; + case 242: + return "\\xF2"; + case 243: + return "\\xF3"; + case 244: + return "\\xF4"; + case 245: + return "\\xF5"; + case 246: + return "\\xF6"; + case 247: + return "\\xF7"; + case 248: + return "\\xF8"; + case 249: + return "\\xF9"; + case 250: + return "\\xFA"; + case 251: + return "\\xFB"; + case 252: + return "\\xFC"; + case 253: + return "\\xFD"; + case 254: + return "\\xFE"; + case 255: + return "\\xFF"; + // LCOV_EXCL_START + default: + assert(0); /* never gets here */ + return "dead code"; + } + assert(0); /* never gets here */ + // LCOV_EXCL_STOP +} + +#endif /* XML_GE == 1 */ + +static unsigned long +getDebugLevel(const char *variableName, unsigned long defaultDebugLevel) { + const char *const valueOrNull = getenv(variableName); + if (valueOrNull == NULL) { + return defaultDebugLevel; + } + const char *const value = valueOrNull; + + errno = 0; + char *afterValue = NULL; + unsigned long debugLevel = strtoul(value, &afterValue, 10); + if ((errno != 0) || (afterValue == value) || (afterValue[0] != '\0')) { + errno = 0; + return defaultDebugLevel; + } + + return debugLevel; +} diff --git a/port/third_party/expat/xmlrole.c b/port/third_party/expat/xmlrole.c new file mode 100644 index 00000000..dbf4a2d1 --- /dev/null +++ b/port/third_party/expat/xmlrole.c @@ -0,0 +1,1256 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Greg Stein + Copyright (c) 2002-2006 Karl Waclawek + Copyright (c) 2002-2003 Fred L. Drake, Jr. + Copyright (c) 2005-2009 Steven Solie + Copyright (c) 2016-2026 Sebastian Pipping + Copyright (c) 2017 Rhodri James + Copyright (c) 2019 David Loffredo + Copyright (c) 2021 Donghee Na + Copyright (c) 2025 Alfonso Gregory + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#include "expat_config.h" + +#include + +#ifdef _WIN32 +# include "winconfig.h" +#endif + +#include "internal.h" +#include "xmlrole.h" +#include "ascii.h" + +/* Doesn't check: + + that ,| are not mixed in a model group + content of literals + +*/ + +static const char KW_ANY[] = {ASCII_A, ASCII_N, ASCII_Y, '\0'}; +static const char KW_ATTLIST[] + = {ASCII_A, ASCII_T, ASCII_T, ASCII_L, ASCII_I, ASCII_S, ASCII_T, '\0'}; +static const char KW_CDATA[] + = {ASCII_C, ASCII_D, ASCII_A, ASCII_T, ASCII_A, '\0'}; +static const char KW_DOCTYPE[] + = {ASCII_D, ASCII_O, ASCII_C, ASCII_T, ASCII_Y, ASCII_P, ASCII_E, '\0'}; +static const char KW_ELEMENT[] + = {ASCII_E, ASCII_L, ASCII_E, ASCII_M, ASCII_E, ASCII_N, ASCII_T, '\0'}; +static const char KW_EMPTY[] + = {ASCII_E, ASCII_M, ASCII_P, ASCII_T, ASCII_Y, '\0'}; +static const char KW_ENTITIES[] = {ASCII_E, ASCII_N, ASCII_T, ASCII_I, ASCII_T, + ASCII_I, ASCII_E, ASCII_S, '\0'}; +static const char KW_ENTITY[] + = {ASCII_E, ASCII_N, ASCII_T, ASCII_I, ASCII_T, ASCII_Y, '\0'}; +static const char KW_FIXED[] + = {ASCII_F, ASCII_I, ASCII_X, ASCII_E, ASCII_D, '\0'}; +static const char KW_ID[] = {ASCII_I, ASCII_D, '\0'}; +static const char KW_IDREF[] + = {ASCII_I, ASCII_D, ASCII_R, ASCII_E, ASCII_F, '\0'}; +static const char KW_IDREFS[] + = {ASCII_I, ASCII_D, ASCII_R, ASCII_E, ASCII_F, ASCII_S, '\0'}; +#ifdef XML_DTD +static const char KW_IGNORE[] + = {ASCII_I, ASCII_G, ASCII_N, ASCII_O, ASCII_R, ASCII_E, '\0'}; +#endif +static const char KW_IMPLIED[] + = {ASCII_I, ASCII_M, ASCII_P, ASCII_L, ASCII_I, ASCII_E, ASCII_D, '\0'}; +#ifdef XML_DTD +static const char KW_INCLUDE[] + = {ASCII_I, ASCII_N, ASCII_C, ASCII_L, ASCII_U, ASCII_D, ASCII_E, '\0'}; +#endif +static const char KW_NDATA[] + = {ASCII_N, ASCII_D, ASCII_A, ASCII_T, ASCII_A, '\0'}; +static const char KW_NMTOKEN[] + = {ASCII_N, ASCII_M, ASCII_T, ASCII_O, ASCII_K, ASCII_E, ASCII_N, '\0'}; +static const char KW_NMTOKENS[] = {ASCII_N, ASCII_M, ASCII_T, ASCII_O, ASCII_K, + ASCII_E, ASCII_N, ASCII_S, '\0'}; +static const char KW_NOTATION[] = {ASCII_N, ASCII_O, ASCII_T, ASCII_A, ASCII_T, + ASCII_I, ASCII_O, ASCII_N, '\0'}; +static const char KW_PCDATA[] + = {ASCII_P, ASCII_C, ASCII_D, ASCII_A, ASCII_T, ASCII_A, '\0'}; +static const char KW_PUBLIC[] + = {ASCII_P, ASCII_U, ASCII_B, ASCII_L, ASCII_I, ASCII_C, '\0'}; +static const char KW_REQUIRED[] = {ASCII_R, ASCII_E, ASCII_Q, ASCII_U, ASCII_I, + ASCII_R, ASCII_E, ASCII_D, '\0'}; +static const char KW_SYSTEM[] + = {ASCII_S, ASCII_Y, ASCII_S, ASCII_T, ASCII_E, ASCII_M, '\0'}; + +#ifndef MIN_BYTES_PER_CHAR +# define MIN_BYTES_PER_CHAR(enc) ((enc)->minBytesPerChar) +#endif + +#ifdef XML_DTD +# define setTopLevel(state) \ + ((state)->handler \ + = ((state)->documentEntity ? internalSubset : externalSubset1)) +#else /* not XML_DTD */ +# define setTopLevel(state) ((state)->handler = internalSubset) +#endif /* not XML_DTD */ + +typedef int PROLOG_HANDLER(PROLOG_STATE *state, int tok, const char *ptr, + const char *end, const ENCODING *enc); + +static PROLOG_HANDLER prolog0, prolog1, prolog2, doctype0, doctype1, doctype2, + doctype3, doctype4, doctype5, internalSubset, entity0, entity1, entity2, + entity3, entity4, entity5, entity6, entity7, entity8, entity9, entity10, + notation0, notation1, notation2, notation3, notation4, attlist0, attlist1, + attlist2, attlist3, attlist4, attlist5, attlist6, attlist7, attlist8, + attlist9, element0, element1, element2, element3, element4, element5, + element6, element7, +#ifdef XML_DTD + externalSubset0, externalSubset1, condSect0, condSect1, condSect2, +#endif /* XML_DTD */ + declClose, error; + +static int common(PROLOG_STATE *state, int tok); + +static int +prolog0(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + state->handler = prolog1; + return XML_ROLE_NONE; + case XML_TOK_XML_DECL: + state->handler = prolog1; + return XML_ROLE_XML_DECL; + case XML_TOK_PI: + state->handler = prolog1; + return XML_ROLE_PI; + case XML_TOK_COMMENT: + state->handler = prolog1; + return XML_ROLE_COMMENT; + case XML_TOK_BOM: + return XML_ROLE_NONE; + case XML_TOK_DECL_OPEN: + if (! XmlNameMatchesAscii(enc, ptr + 2 * MIN_BYTES_PER_CHAR(enc), end, + KW_DOCTYPE)) + break; + state->handler = doctype0; + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_INSTANCE_START: + state->handler = error; + return XML_ROLE_INSTANCE_START; + } + return common(state, tok); +} + +static int +prolog1(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NONE; + case XML_TOK_PI: + return XML_ROLE_PI; + case XML_TOK_COMMENT: + return XML_ROLE_COMMENT; + case XML_TOK_BOM: + /* This case can never arise. To reach this role function, the + * parse must have passed through prolog0 and therefore have had + * some form of input, even if only a space. At that point, a + * byte order mark is no longer a valid character (though + * technically it should be interpreted as a non-breaking space), + * so will be rejected by the tokenizing stages. + */ + return XML_ROLE_NONE; /* LCOV_EXCL_LINE */ + case XML_TOK_DECL_OPEN: + if (! XmlNameMatchesAscii(enc, ptr + 2 * MIN_BYTES_PER_CHAR(enc), end, + KW_DOCTYPE)) + break; + state->handler = doctype0; + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_INSTANCE_START: + state->handler = error; + return XML_ROLE_INSTANCE_START; + } + return common(state, tok); +} + +static int +prolog2(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NONE; + case XML_TOK_PI: + return XML_ROLE_PI; + case XML_TOK_COMMENT: + return XML_ROLE_COMMENT; + case XML_TOK_INSTANCE_START: + state->handler = error; + return XML_ROLE_INSTANCE_START; + } + return common(state, tok); +} + +static int +doctype0(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_NAME: + case XML_TOK_PREFIXED_NAME: + state->handler = doctype1; + return XML_ROLE_DOCTYPE_NAME; + } + return common(state, tok); +} + +static int +doctype1(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_OPEN_BRACKET: + state->handler = internalSubset; + return XML_ROLE_DOCTYPE_INTERNAL_SUBSET; + case XML_TOK_DECL_CLOSE: + state->handler = prolog2; + return XML_ROLE_DOCTYPE_CLOSE; + case XML_TOK_NAME: + if (XmlNameMatchesAscii(enc, ptr, end, KW_SYSTEM)) { + state->handler = doctype3; + return XML_ROLE_DOCTYPE_NONE; + } + if (XmlNameMatchesAscii(enc, ptr, end, KW_PUBLIC)) { + state->handler = doctype2; + return XML_ROLE_DOCTYPE_NONE; + } + break; + } + return common(state, tok); +} + +static int +doctype2(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_LITERAL: + state->handler = doctype3; + return XML_ROLE_DOCTYPE_PUBLIC_ID; + } + return common(state, tok); +} + +static int +doctype3(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_LITERAL: + state->handler = doctype4; + return XML_ROLE_DOCTYPE_SYSTEM_ID; + } + return common(state, tok); +} + +static int +doctype4(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_OPEN_BRACKET: + state->handler = internalSubset; + return XML_ROLE_DOCTYPE_INTERNAL_SUBSET; + case XML_TOK_DECL_CLOSE: + state->handler = prolog2; + return XML_ROLE_DOCTYPE_CLOSE; + } + return common(state, tok); +} + +static int +doctype5(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_DECL_CLOSE: + state->handler = prolog2; + return XML_ROLE_DOCTYPE_CLOSE; + } + return common(state, tok); +} + +static int +internalSubset(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NONE; + case XML_TOK_DECL_OPEN: + if (XmlNameMatchesAscii(enc, ptr + 2 * MIN_BYTES_PER_CHAR(enc), end, + KW_ENTITY)) { + state->handler = entity0; + return XML_ROLE_ENTITY_NONE; + } + if (XmlNameMatchesAscii(enc, ptr + 2 * MIN_BYTES_PER_CHAR(enc), end, + KW_ATTLIST)) { + state->handler = attlist0; + return XML_ROLE_ATTLIST_NONE; + } + if (XmlNameMatchesAscii(enc, ptr + 2 * MIN_BYTES_PER_CHAR(enc), end, + KW_ELEMENT)) { + state->handler = element0; + return XML_ROLE_ELEMENT_NONE; + } + if (XmlNameMatchesAscii(enc, ptr + 2 * MIN_BYTES_PER_CHAR(enc), end, + KW_NOTATION)) { + state->handler = notation0; + return XML_ROLE_NOTATION_NONE; + } + break; + case XML_TOK_PI: + return XML_ROLE_PI; + case XML_TOK_COMMENT: + return XML_ROLE_COMMENT; + case XML_TOK_PARAM_ENTITY_REF: + return XML_ROLE_PARAM_ENTITY_REF; + case XML_TOK_CLOSE_BRACKET: + state->handler = doctype5; + return XML_ROLE_DOCTYPE_NONE; + case XML_TOK_NONE: + return XML_ROLE_NONE; + } + return common(state, tok); +} + +#ifdef XML_DTD + +static int +externalSubset0(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + state->handler = externalSubset1; + if (tok == XML_TOK_XML_DECL) + return XML_ROLE_TEXT_DECL; + return externalSubset1(state, tok, ptr, end, enc); +} + +static int +externalSubset1(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_COND_SECT_OPEN: + state->handler = condSect0; + return XML_ROLE_NONE; + case XML_TOK_COND_SECT_CLOSE: + if (state->includeLevel == 0) + break; + state->includeLevel -= 1; + return XML_ROLE_NONE; + case XML_TOK_PROLOG_S: + return XML_ROLE_NONE; + case XML_TOK_CLOSE_BRACKET: + break; + case XML_TOK_NONE: + if (state->includeLevel) + break; + return XML_ROLE_NONE; + default: + return internalSubset(state, tok, ptr, end, enc); + } + return common(state, tok); +} + +#endif /* XML_DTD */ + +static int +entity0(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_PERCENT: + state->handler = entity1; + return XML_ROLE_ENTITY_NONE; + case XML_TOK_NAME: + state->handler = entity2; + return XML_ROLE_GENERAL_ENTITY_NAME; + } + return common(state, tok); +} + +static int +entity1(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_NAME: + state->handler = entity7; + return XML_ROLE_PARAM_ENTITY_NAME; + } + return common(state, tok); +} + +static int +entity2(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_NAME: + if (XmlNameMatchesAscii(enc, ptr, end, KW_SYSTEM)) { + state->handler = entity4; + return XML_ROLE_ENTITY_NONE; + } + if (XmlNameMatchesAscii(enc, ptr, end, KW_PUBLIC)) { + state->handler = entity3; + return XML_ROLE_ENTITY_NONE; + } + break; + case XML_TOK_LITERAL: + state->handler = declClose; + state->role_none = XML_ROLE_ENTITY_NONE; + return XML_ROLE_ENTITY_VALUE; + } + return common(state, tok); +} + +static int +entity3(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_LITERAL: + state->handler = entity4; + return XML_ROLE_ENTITY_PUBLIC_ID; + } + return common(state, tok); +} + +static int +entity4(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_LITERAL: + state->handler = entity5; + return XML_ROLE_ENTITY_SYSTEM_ID; + } + return common(state, tok); +} + +static int +entity5(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_DECL_CLOSE: + setTopLevel(state); + return XML_ROLE_ENTITY_COMPLETE; + case XML_TOK_NAME: + if (XmlNameMatchesAscii(enc, ptr, end, KW_NDATA)) { + state->handler = entity6; + return XML_ROLE_ENTITY_NONE; + } + break; + } + return common(state, tok); +} + +static int +entity6(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_NAME: + state->handler = declClose; + state->role_none = XML_ROLE_ENTITY_NONE; + return XML_ROLE_ENTITY_NOTATION_NAME; + } + return common(state, tok); +} + +static int +entity7(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_NAME: + if (XmlNameMatchesAscii(enc, ptr, end, KW_SYSTEM)) { + state->handler = entity9; + return XML_ROLE_ENTITY_NONE; + } + if (XmlNameMatchesAscii(enc, ptr, end, KW_PUBLIC)) { + state->handler = entity8; + return XML_ROLE_ENTITY_NONE; + } + break; + case XML_TOK_LITERAL: + state->handler = declClose; + state->role_none = XML_ROLE_ENTITY_NONE; + return XML_ROLE_ENTITY_VALUE; + } + return common(state, tok); +} + +static int +entity8(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_LITERAL: + state->handler = entity9; + return XML_ROLE_ENTITY_PUBLIC_ID; + } + return common(state, tok); +} + +static int +entity9(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_LITERAL: + state->handler = entity10; + return XML_ROLE_ENTITY_SYSTEM_ID; + } + return common(state, tok); +} + +static int +entity10(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ENTITY_NONE; + case XML_TOK_DECL_CLOSE: + setTopLevel(state); + return XML_ROLE_ENTITY_COMPLETE; + } + return common(state, tok); +} + +static int +notation0(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NOTATION_NONE; + case XML_TOK_NAME: + state->handler = notation1; + return XML_ROLE_NOTATION_NAME; + } + return common(state, tok); +} + +static int +notation1(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NOTATION_NONE; + case XML_TOK_NAME: + if (XmlNameMatchesAscii(enc, ptr, end, KW_SYSTEM)) { + state->handler = notation3; + return XML_ROLE_NOTATION_NONE; + } + if (XmlNameMatchesAscii(enc, ptr, end, KW_PUBLIC)) { + state->handler = notation2; + return XML_ROLE_NOTATION_NONE; + } + break; + } + return common(state, tok); +} + +static int +notation2(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NOTATION_NONE; + case XML_TOK_LITERAL: + state->handler = notation4; + return XML_ROLE_NOTATION_PUBLIC_ID; + } + return common(state, tok); +} + +static int +notation3(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NOTATION_NONE; + case XML_TOK_LITERAL: + state->handler = declClose; + state->role_none = XML_ROLE_NOTATION_NONE; + return XML_ROLE_NOTATION_SYSTEM_ID; + } + return common(state, tok); +} + +static int +notation4(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NOTATION_NONE; + case XML_TOK_LITERAL: + state->handler = declClose; + state->role_none = XML_ROLE_NOTATION_NONE; + return XML_ROLE_NOTATION_SYSTEM_ID; + case XML_TOK_DECL_CLOSE: + setTopLevel(state); + return XML_ROLE_NOTATION_NO_SYSTEM_ID; + } + return common(state, tok); +} + +static int +attlist0(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_NAME: + case XML_TOK_PREFIXED_NAME: + state->handler = attlist1; + return XML_ROLE_ATTLIST_ELEMENT_NAME; + } + return common(state, tok); +} + +static int +attlist1(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_DECL_CLOSE: + setTopLevel(state); + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_NAME: + case XML_TOK_PREFIXED_NAME: + state->handler = attlist2; + return XML_ROLE_ATTRIBUTE_NAME; + } + return common(state, tok); +} + +static int +attlist2(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_NAME: { + static const char *const types[] = { + KW_CDATA, KW_ID, KW_IDREF, KW_IDREFS, + KW_ENTITY, KW_ENTITIES, KW_NMTOKEN, KW_NMTOKENS, + }; + int i; + for (i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) + if (XmlNameMatchesAscii(enc, ptr, end, types[i])) { + state->handler = attlist8; + return XML_ROLE_ATTRIBUTE_TYPE_CDATA + i; + } + } + if (XmlNameMatchesAscii(enc, ptr, end, KW_NOTATION)) { + state->handler = attlist5; + return XML_ROLE_ATTLIST_NONE; + } + break; + case XML_TOK_OPEN_PAREN: + state->handler = attlist3; + return XML_ROLE_ATTLIST_NONE; + } + return common(state, tok); +} + +static int +attlist3(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_NMTOKEN: + case XML_TOK_NAME: + case XML_TOK_PREFIXED_NAME: + state->handler = attlist4; + return XML_ROLE_ATTRIBUTE_ENUM_VALUE; + } + return common(state, tok); +} + +static int +attlist4(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_CLOSE_PAREN: + state->handler = attlist8; + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_OR: + state->handler = attlist3; + return XML_ROLE_ATTLIST_NONE; + } + return common(state, tok); +} + +static int +attlist5(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_OPEN_PAREN: + state->handler = attlist6; + return XML_ROLE_ATTLIST_NONE; + } + return common(state, tok); +} + +static int +attlist6(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_NAME: + state->handler = attlist7; + return XML_ROLE_ATTRIBUTE_NOTATION_VALUE; + } + return common(state, tok); +} + +static int +attlist7(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_CLOSE_PAREN: + state->handler = attlist8; + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_OR: + state->handler = attlist6; + return XML_ROLE_ATTLIST_NONE; + } + return common(state, tok); +} + +/* default value */ +static int +attlist8(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_POUND_NAME: + if (XmlNameMatchesAscii(enc, ptr + MIN_BYTES_PER_CHAR(enc), end, + KW_IMPLIED)) { + state->handler = attlist1; + return XML_ROLE_IMPLIED_ATTRIBUTE_VALUE; + } + if (XmlNameMatchesAscii(enc, ptr + MIN_BYTES_PER_CHAR(enc), end, + KW_REQUIRED)) { + state->handler = attlist1; + return XML_ROLE_REQUIRED_ATTRIBUTE_VALUE; + } + if (XmlNameMatchesAscii(enc, ptr + MIN_BYTES_PER_CHAR(enc), end, + KW_FIXED)) { + state->handler = attlist9; + return XML_ROLE_ATTLIST_NONE; + } + break; + case XML_TOK_LITERAL: + state->handler = attlist1; + return XML_ROLE_DEFAULT_ATTRIBUTE_VALUE; + } + return common(state, tok); +} + +static int +attlist9(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ATTLIST_NONE; + case XML_TOK_LITERAL: + state->handler = attlist1; + return XML_ROLE_FIXED_ATTRIBUTE_VALUE; + } + return common(state, tok); +} + +static int +element0(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ELEMENT_NONE; + case XML_TOK_NAME: + case XML_TOK_PREFIXED_NAME: + state->handler = element1; + return XML_ROLE_ELEMENT_NAME; + } + return common(state, tok); +} + +static int +element1(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ELEMENT_NONE; + case XML_TOK_NAME: + if (XmlNameMatchesAscii(enc, ptr, end, KW_EMPTY)) { + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + return XML_ROLE_CONTENT_EMPTY; + } + if (XmlNameMatchesAscii(enc, ptr, end, KW_ANY)) { + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + return XML_ROLE_CONTENT_ANY; + } + break; + case XML_TOK_OPEN_PAREN: + state->handler = element2; + state->level = 1; + return XML_ROLE_GROUP_OPEN; + } + return common(state, tok); +} + +static int +element2(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ELEMENT_NONE; + case XML_TOK_POUND_NAME: + if (XmlNameMatchesAscii(enc, ptr + MIN_BYTES_PER_CHAR(enc), end, + KW_PCDATA)) { + state->handler = element3; + return XML_ROLE_CONTENT_PCDATA; + } + break; + case XML_TOK_OPEN_PAREN: + state->level = 2; + state->handler = element6; + return XML_ROLE_GROUP_OPEN; + case XML_TOK_NAME: + case XML_TOK_PREFIXED_NAME: + state->handler = element7; + return XML_ROLE_CONTENT_ELEMENT; + case XML_TOK_NAME_QUESTION: + state->handler = element7; + return XML_ROLE_CONTENT_ELEMENT_OPT; + case XML_TOK_NAME_ASTERISK: + state->handler = element7; + return XML_ROLE_CONTENT_ELEMENT_REP; + case XML_TOK_NAME_PLUS: + state->handler = element7; + return XML_ROLE_CONTENT_ELEMENT_PLUS; + } + return common(state, tok); +} + +static int +element3(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ELEMENT_NONE; + case XML_TOK_CLOSE_PAREN: + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + return XML_ROLE_GROUP_CLOSE; + case XML_TOK_CLOSE_PAREN_ASTERISK: + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + return XML_ROLE_GROUP_CLOSE_REP; + case XML_TOK_OR: + state->handler = element4; + return XML_ROLE_ELEMENT_NONE; + } + return common(state, tok); +} + +static int +element4(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ELEMENT_NONE; + case XML_TOK_NAME: + case XML_TOK_PREFIXED_NAME: + state->handler = element5; + return XML_ROLE_CONTENT_ELEMENT; + } + return common(state, tok); +} + +static int +element5(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ELEMENT_NONE; + case XML_TOK_CLOSE_PAREN_ASTERISK: + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + return XML_ROLE_GROUP_CLOSE_REP; + case XML_TOK_OR: + state->handler = element4; + return XML_ROLE_ELEMENT_NONE; + } + return common(state, tok); +} + +static int +element6(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ELEMENT_NONE; + case XML_TOK_OPEN_PAREN: + state->level += 1; + return XML_ROLE_GROUP_OPEN; + case XML_TOK_NAME: + case XML_TOK_PREFIXED_NAME: + state->handler = element7; + return XML_ROLE_CONTENT_ELEMENT; + case XML_TOK_NAME_QUESTION: + state->handler = element7; + return XML_ROLE_CONTENT_ELEMENT_OPT; + case XML_TOK_NAME_ASTERISK: + state->handler = element7; + return XML_ROLE_CONTENT_ELEMENT_REP; + case XML_TOK_NAME_PLUS: + state->handler = element7; + return XML_ROLE_CONTENT_ELEMENT_PLUS; + } + return common(state, tok); +} + +static int +element7(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_ELEMENT_NONE; + case XML_TOK_CLOSE_PAREN: + state->level -= 1; + if (state->level == 0) { + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + } + return XML_ROLE_GROUP_CLOSE; + case XML_TOK_CLOSE_PAREN_ASTERISK: + state->level -= 1; + if (state->level == 0) { + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + } + return XML_ROLE_GROUP_CLOSE_REP; + case XML_TOK_CLOSE_PAREN_QUESTION: + state->level -= 1; + if (state->level == 0) { + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + } + return XML_ROLE_GROUP_CLOSE_OPT; + case XML_TOK_CLOSE_PAREN_PLUS: + state->level -= 1; + if (state->level == 0) { + state->handler = declClose; + state->role_none = XML_ROLE_ELEMENT_NONE; + } + return XML_ROLE_GROUP_CLOSE_PLUS; + case XML_TOK_COMMA: + state->handler = element6; + return XML_ROLE_GROUP_SEQUENCE; + case XML_TOK_OR: + state->handler = element6; + return XML_ROLE_GROUP_CHOICE; + } + return common(state, tok); +} + +#ifdef XML_DTD + +static int +condSect0(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NONE; + case XML_TOK_NAME: + if (XmlNameMatchesAscii(enc, ptr, end, KW_INCLUDE)) { + state->handler = condSect1; + return XML_ROLE_NONE; + } + if (XmlNameMatchesAscii(enc, ptr, end, KW_IGNORE)) { + state->handler = condSect2; + return XML_ROLE_NONE; + } + break; + } + return common(state, tok); +} + +static int +condSect1(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NONE; + case XML_TOK_OPEN_BRACKET: + state->handler = externalSubset1; + state->includeLevel += 1; + return XML_ROLE_NONE; + } + return common(state, tok); +} + +static int +condSect2(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return XML_ROLE_NONE; + case XML_TOK_OPEN_BRACKET: + state->handler = externalSubset1; + return XML_ROLE_IGNORE_SECT; + } + return common(state, tok); +} + +#endif /* XML_DTD */ + +static int +declClose(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + switch (tok) { + case XML_TOK_PROLOG_S: + return state->role_none; + case XML_TOK_DECL_CLOSE: + setTopLevel(state); + return state->role_none; + } + return common(state, tok); +} + +/* This function will only be invoked if the internal logic of the + * parser has broken down. It is used in two cases: + * + * 1: When the XML prolog has been finished. At this point the + * processor (the parser level above these role handlers) should + * switch from prologProcessor to contentProcessor and reinitialise + * the handler function. + * + * 2: When an error has been detected (via common() below). At this + * point again the processor should be switched to errorProcessor, + * which will never call a handler. + * + * The result of this is that error() can only be called if the + * processor switch failed to happen, which is an internal error and + * therefore we shouldn't be able to provoke it simply by using the + * library. It is a necessary backstop, however, so we merely exclude + * it from the coverage statistics. + * + * LCOV_EXCL_START + */ +static int +error(PROLOG_STATE *state, int tok, const char *ptr, const char *end, + const ENCODING *enc) { + UNUSED_P(state); + UNUSED_P(tok); + UNUSED_P(ptr); + UNUSED_P(end); + UNUSED_P(enc); + return XML_ROLE_NONE; +} +/* LCOV_EXCL_STOP */ + +static int +common(PROLOG_STATE *state, int tok) { +#ifdef XML_DTD + if (! state->documentEntity && tok == XML_TOK_PARAM_ENTITY_REF) + return XML_ROLE_INNER_PARAM_ENTITY_REF; +#else + UNUSED_P(tok); +#endif + state->handler = error; + return XML_ROLE_ERROR; +} + +void +XmlPrologStateInit(PROLOG_STATE *state) { + state->handler = prolog0; +#ifdef XML_DTD + state->documentEntity = 1; + state->includeLevel = 0; + state->inEntityValue = 0; +#endif /* XML_DTD */ +} + +#ifdef XML_DTD + +void +XmlPrologStateInitExternalEntity(PROLOG_STATE *state) { + state->handler = externalSubset0; + state->documentEntity = 0; + state->includeLevel = 0; +} + +#endif /* XML_DTD */ diff --git a/port/third_party/expat/xmlrole.h b/port/third_party/expat/xmlrole.h new file mode 100644 index 00000000..f4aba407 --- /dev/null +++ b/port/third_party/expat/xmlrole.h @@ -0,0 +1,130 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Karl Waclawek + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2017-2026 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#ifndef XmlRole_INCLUDED +# define XmlRole_INCLUDED 1 + +# include "xmltok.h" + +enum { + XML_ROLE_ERROR = -1, + XML_ROLE_NONE = 0, + XML_ROLE_XML_DECL, + XML_ROLE_INSTANCE_START, + XML_ROLE_DOCTYPE_NONE, + XML_ROLE_DOCTYPE_NAME, + XML_ROLE_DOCTYPE_SYSTEM_ID, + XML_ROLE_DOCTYPE_PUBLIC_ID, + XML_ROLE_DOCTYPE_INTERNAL_SUBSET, + XML_ROLE_DOCTYPE_CLOSE, + XML_ROLE_GENERAL_ENTITY_NAME, + XML_ROLE_PARAM_ENTITY_NAME, + XML_ROLE_ENTITY_NONE, + XML_ROLE_ENTITY_VALUE, + XML_ROLE_ENTITY_SYSTEM_ID, + XML_ROLE_ENTITY_PUBLIC_ID, + XML_ROLE_ENTITY_COMPLETE, + XML_ROLE_ENTITY_NOTATION_NAME, + XML_ROLE_NOTATION_NONE, + XML_ROLE_NOTATION_NAME, + XML_ROLE_NOTATION_SYSTEM_ID, + XML_ROLE_NOTATION_NO_SYSTEM_ID, + XML_ROLE_NOTATION_PUBLIC_ID, + XML_ROLE_ATTRIBUTE_NAME, + XML_ROLE_ATTRIBUTE_TYPE_CDATA, + XML_ROLE_ATTRIBUTE_TYPE_ID, + XML_ROLE_ATTRIBUTE_TYPE_IDREF, + XML_ROLE_ATTRIBUTE_TYPE_IDREFS, + XML_ROLE_ATTRIBUTE_TYPE_ENTITY, + XML_ROLE_ATTRIBUTE_TYPE_ENTITIES, + XML_ROLE_ATTRIBUTE_TYPE_NMTOKEN, + XML_ROLE_ATTRIBUTE_TYPE_NMTOKENS, + XML_ROLE_ATTRIBUTE_ENUM_VALUE, + XML_ROLE_ATTRIBUTE_NOTATION_VALUE, + XML_ROLE_ATTLIST_NONE, + XML_ROLE_ATTLIST_ELEMENT_NAME, + XML_ROLE_IMPLIED_ATTRIBUTE_VALUE, + XML_ROLE_REQUIRED_ATTRIBUTE_VALUE, + XML_ROLE_DEFAULT_ATTRIBUTE_VALUE, + XML_ROLE_FIXED_ATTRIBUTE_VALUE, + XML_ROLE_ELEMENT_NONE, + XML_ROLE_ELEMENT_NAME, + XML_ROLE_CONTENT_ANY, + XML_ROLE_CONTENT_EMPTY, + XML_ROLE_CONTENT_PCDATA, + XML_ROLE_GROUP_OPEN, + XML_ROLE_GROUP_CLOSE, + XML_ROLE_GROUP_CLOSE_REP, + XML_ROLE_GROUP_CLOSE_OPT, + XML_ROLE_GROUP_CLOSE_PLUS, + XML_ROLE_GROUP_CHOICE, + XML_ROLE_GROUP_SEQUENCE, + XML_ROLE_CONTENT_ELEMENT, + XML_ROLE_CONTENT_ELEMENT_REP, + XML_ROLE_CONTENT_ELEMENT_OPT, + XML_ROLE_CONTENT_ELEMENT_PLUS, + XML_ROLE_PI, + XML_ROLE_COMMENT, +# ifdef XML_DTD + XML_ROLE_TEXT_DECL, + XML_ROLE_IGNORE_SECT, + XML_ROLE_INNER_PARAM_ENTITY_REF, +# endif /* XML_DTD */ + XML_ROLE_PARAM_ENTITY_REF +}; + +typedef struct prolog_state { + int (*handler)(struct prolog_state *state, int tok, const char *ptr, + const char *end, const ENCODING *enc); + unsigned level; + int role_none; +# ifdef XML_DTD + unsigned includeLevel; + int documentEntity; + int inEntityValue; +# endif /* XML_DTD */ +} PROLOG_STATE; + +void XmlPrologStateInit(PROLOG_STATE *state); +# ifdef XML_DTD +void XmlPrologStateInitExternalEntity(PROLOG_STATE *state); +# endif /* XML_DTD */ + +# define XmlTokenRole(state, tok, ptr, end, enc) \ + (((state)->handler)(state, tok, ptr, end, enc)) + +#endif /* not XmlRole_INCLUDED */ diff --git a/port/third_party/expat/xmltok.c b/port/third_party/expat/xmltok.c new file mode 100644 index 00000000..a3bf5e0e --- /dev/null +++ b/port/third_party/expat/xmltok.c @@ -0,0 +1,1760 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2001-2003 Fred L. Drake, Jr. + Copyright (c) 2002 Greg Stein + Copyright (c) 2002-2016 Karl Waclawek + Copyright (c) 2005-2009 Steven Solie + Copyright (c) 2016-2026 Sebastian Pipping + Copyright (c) 2016 Pascal Cuoq + Copyright (c) 2016 Don Lewis + Copyright (c) 2017 Rhodri James + Copyright (c) 2017 Alexander Bluhm + Copyright (c) 2017 Benbuck Nason + Copyright (c) 2017 José Gutiérrez de la Concha + Copyright (c) 2019 David Loffredo + Copyright (c) 2021 Donghee Na + Copyright (c) 2022 Martin Ettl + Copyright (c) 2022 Sean McBride + Copyright (c) 2023 Hanno Böck + Copyright (c) 2025 Alfonso Gregory + Copyright (c) 2026 Nick Begg + Copyright (c) 2026 Kartik Kenchi + Copyright (c) 2026 Afonso Januário + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#include "expat_config.h" + +#include +#include /* memcpy */ +#include + +#ifdef _WIN32 +# include "winconfig.h" +#endif + +#include "internal.h" +#include "fallthrough.h" +#include "xmltok.h" +#include "nametab.h" + +#ifdef XML_DTD +# define IGNORE_SECTION_TOK_VTABLE , PREFIX(ignoreSectionTok) +#else +# define IGNORE_SECTION_TOK_VTABLE /* as nothing */ +#endif + +#define VTABLE1 \ + {PREFIX(prologTok), PREFIX(contentTok), \ + PREFIX(cdataSectionTok) IGNORE_SECTION_TOK_VTABLE}, \ + {PREFIX(attributeValueTok), PREFIX(entityValueTok)}, \ + PREFIX(nameMatchesAscii), PREFIX(nameLength), PREFIX(skipS), \ + PREFIX(getAtts), PREFIX(charRefNumber), PREFIX(predefinedEntityName), \ + PREFIX(updatePosition), PREFIX(isPublicId) + +#define VTABLE VTABLE1, PREFIX(toUtf8), PREFIX(toUtf16) + +#define UCS2_GET_NAMING(pages, hi, lo) \ + (namingBitmap[(pages[hi] << 3) + ((lo) >> 5)] & (1u << ((lo) & 0x1F))) + +/* A 2 byte UTF-8 representation splits the characters 11 bits between + the bottom 5 and 6 bits of the bytes. We need 8 bits to index into + pages, 3 bits to add to that index and 5 bits to generate the mask. +*/ +#define UTF8_GET_NAMING2(pages, byte) \ + (namingBitmap[((pages)[(((byte)[0]) >> 2) & 7] << 3) \ + + ((((byte)[0]) & 3) << 1) + ((((byte)[1]) >> 5) & 1)] \ + & (1u << (((byte)[1]) & 0x1F))) + +/* A 3 byte UTF-8 representation splits the characters 16 bits between + the bottom 4, 6 and 6 bits of the bytes. We need 8 bits to index + into pages, 3 bits to add to that index and 5 bits to generate the + mask. +*/ +#define UTF8_GET_NAMING3(pages, byte) \ + (namingBitmap \ + [((pages)[((((byte)[0]) & 0xF) << 4) + ((((byte)[1]) >> 2) & 0xF)] \ + << 3) \ + + ((((byte)[1]) & 3) << 1) + ((((byte)[2]) >> 5) & 1)] \ + & (1u << (((byte)[2]) & 0x1F))) + +/* Detection of invalid UTF-8 sequences is based on Table 3.1B + of Unicode 3.2: https://www.unicode.org/unicode/reports/tr28/ + with the additional restriction of not allowing the Unicode + code points 0xFFFF and 0xFFFE (sequences EF,BF,BF and EF,BF,BE). + Implementation details: + (A & 0x80) == 0 means A < 0x80 + and + (A & 0xC0) == 0xC0 means A > 0xBF +*/ + +#define UTF8_INVALID2(p) \ + ((*p) < 0xC2 || ((p)[1] & 0x80) == 0 || ((p)[1] & 0xC0) == 0xC0) + +#define UTF8_INVALID3(p) \ + (((p)[2] & 0x80) == 0 \ + || ((*p) == 0xEF && (p)[1] == 0xBF ? (p)[2] > 0xBD \ + : ((p)[2] & 0xC0) == 0xC0) \ + || ((*p) == 0xE0 \ + ? (p)[1] < 0xA0 || ((p)[1] & 0xC0) == 0xC0 \ + : ((p)[1] & 0x80) == 0 \ + || ((*p) == 0xED ? (p)[1] > 0x9F : ((p)[1] & 0xC0) == 0xC0))) + +#define UTF8_INVALID4(p) \ + (((p)[3] & 0x80) == 0 || ((p)[3] & 0xC0) == 0xC0 || ((p)[2] & 0x80) == 0 \ + || ((p)[2] & 0xC0) == 0xC0 \ + || ((*p) == 0xF0 \ + ? (p)[1] < 0x90 || ((p)[1] & 0xC0) == 0xC0 \ + : ((p)[1] & 0x80) == 0 \ + || ((*p) == 0xF4 ? (p)[1] > 0x8F : ((p)[1] & 0xC0) == 0xC0))) + +static int +isNever(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + UNUSED_P(p); + return 0; +} + +static int +utf8_isName2(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return UTF8_GET_NAMING2(namePages, (const unsigned char *)p); +} + +static int +utf8_isName3(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return UTF8_GET_NAMING3(namePages, (const unsigned char *)p); +} + +#define utf8_isName4 isNever + +static int +utf8_isNmstrt2(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return UTF8_GET_NAMING2(nmstrtPages, (const unsigned char *)p); +} + +static int +utf8_isNmstrt3(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return UTF8_GET_NAMING3(nmstrtPages, (const unsigned char *)p); +} + +#define utf8_isNmstrt4 isNever + +static int +utf8_isInvalid2(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return UTF8_INVALID2((const unsigned char *)p); +} + +static int +utf8_isInvalid3(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return UTF8_INVALID3((const unsigned char *)p); +} + +static int +utf8_isInvalid4(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return UTF8_INVALID4((const unsigned char *)p); +} + +struct normal_encoding { + ENCODING enc; + unsigned char type[256]; +#ifdef XML_MIN_SIZE + int (*byteType)(const ENCODING *, const char *); + int (*isNameMin)(const ENCODING *, const char *); + int (*isNmstrtMin)(const ENCODING *, const char *); + int (*byteToAscii)(const ENCODING *, const char *); + int (*charMatches)(const ENCODING *, const char *, int); +#endif /* XML_MIN_SIZE */ + int (*isName2)(const ENCODING *, const char *); + int (*isName3)(const ENCODING *, const char *); + int (*isName4)(const ENCODING *, const char *); + int (*isNmstrt2)(const ENCODING *, const char *); + int (*isNmstrt3)(const ENCODING *, const char *); + int (*isNmstrt4)(const ENCODING *, const char *); + int (*isInvalid2)(const ENCODING *, const char *); + int (*isInvalid3)(const ENCODING *, const char *); + int (*isInvalid4)(const ENCODING *, const char *); +}; + +#define AS_NORMAL_ENCODING(enc) ((const struct normal_encoding *)(enc)) + +#ifdef XML_MIN_SIZE + +# define STANDARD_VTABLE(E) \ + E##byteType, E##isNameMin, E##isNmstrtMin, E##byteToAscii, E##charMatches, + +#else + +# define STANDARD_VTABLE(E) /* as nothing */ + +#endif + +#define NORMAL_VTABLE(E) \ + E##isName2, E##isName3, E##isName4, E##isNmstrt2, E##isNmstrt3, \ + E##isNmstrt4, E##isInvalid2, E##isInvalid3, E##isInvalid4 + +#define NULL_VTABLE \ + /* isName2 */ NULL, /* isName3 */ NULL, /* isName4 */ NULL, \ + /* isNmstrt2 */ NULL, /* isNmstrt3 */ NULL, /* isNmstrt4 */ NULL, \ + /* isInvalid2 */ NULL, /* isInvalid3 */ NULL, /* isInvalid4 */ NULL + +/* Like NULL_VTABLE but with a real isInvalid4 so the UTF-16 encodings reject a + high surrogate that is not followed by a low surrogate. Only needed for the + XML_MIN_SIZE build, where the shared tokenizer dispatches through the vtable; + the regular build inlines the same check via IS_INVALID_CHAR. */ +#ifdef XML_MIN_SIZE +# define UTF16_NULL_VTABLE(E) \ + /* isName2 */ NULL, /* isName3 */ NULL, /* isName4 */ NULL, \ + /* isNmstrt2 */ NULL, /* isNmstrt3 */ NULL, /* isNmstrt4 */ NULL, \ + /* isInvalid2 */ NULL, /* isInvalid3 */ NULL, E##isInvalid4 +#else +# define UTF16_NULL_VTABLE(E) NULL_VTABLE +#endif + +static int checkCharRefNumber(int result); + +#include "xmltok_impl.h" +#include "ascii.h" + +#ifdef XML_MIN_SIZE +# define sb_isNameMin isNever +# define sb_isNmstrtMin isNever +#endif + +#ifdef XML_MIN_SIZE +# define MINBPC(enc) ((enc)->minBytesPerChar) +#else +/* minimum bytes per character */ +# define MINBPC(enc) 1 +#endif + +#define SB_BYTE_TYPE(enc, p) \ + (((const struct normal_encoding *)(enc))->type[(unsigned char)*(p)]) + +#ifdef XML_MIN_SIZE +static int +sb_byteType(const ENCODING *enc, const char *p) { + return SB_BYTE_TYPE(enc, p); +} +# define BYTE_TYPE(enc, p) (AS_NORMAL_ENCODING(enc)->byteType(enc, p)) +#else +# define BYTE_TYPE(enc, p) SB_BYTE_TYPE(enc, p) +#endif + +#ifdef XML_MIN_SIZE +# define BYTE_TO_ASCII(enc, p) (AS_NORMAL_ENCODING(enc)->byteToAscii(enc, p)) +static int +sb_byteToAscii(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return *p; +} +#else +# define BYTE_TO_ASCII(enc, p) (*(p)) +#endif + +#define IS_NAME_CHAR(enc, p, n) (AS_NORMAL_ENCODING(enc)->isName##n(enc, p)) +#define IS_NMSTRT_CHAR(enc, p, n) (AS_NORMAL_ENCODING(enc)->isNmstrt##n(enc, p)) +#ifdef XML_MIN_SIZE +# define IS_INVALID_CHAR(enc, p, n) \ + (AS_NORMAL_ENCODING(enc)->isInvalid##n \ + && AS_NORMAL_ENCODING(enc)->isInvalid##n(enc, p)) +#else +# define IS_INVALID_CHAR(enc, p, n) \ + (AS_NORMAL_ENCODING(enc)->isInvalid##n(enc, p)) +#endif + +#ifdef XML_MIN_SIZE +# define IS_NAME_CHAR_MINBPC(enc, p) \ + (AS_NORMAL_ENCODING(enc)->isNameMin(enc, p)) +# define IS_NMSTRT_CHAR_MINBPC(enc, p) \ + (AS_NORMAL_ENCODING(enc)->isNmstrtMin(enc, p)) +#else +# define IS_NAME_CHAR_MINBPC(enc, p) (0) +# define IS_NMSTRT_CHAR_MINBPC(enc, p) (0) +#endif + +#ifdef XML_MIN_SIZE +# define CHAR_MATCHES(enc, p, c) \ + (AS_NORMAL_ENCODING(enc)->charMatches(enc, p, c)) +static int +sb_charMatches(const ENCODING *enc, const char *p, int c) { + UNUSED_P(enc); + return *p == c; +} +#else +/* c is an ASCII character */ +# define CHAR_MATCHES(enc, p, c) (*(p) == (c)) +#endif + +#define PREFIX(ident) normal_##ident +#define XML_TOK_IMPL_C +#include "xmltok_impl.c" +#undef XML_TOK_IMPL_C + +#undef MINBPC +#undef BYTE_TYPE +#undef BYTE_TO_ASCII +#undef CHAR_MATCHES +#undef IS_NAME_CHAR +#undef IS_NAME_CHAR_MINBPC +#undef IS_NMSTRT_CHAR +#undef IS_NMSTRT_CHAR_MINBPC +#undef IS_INVALID_CHAR + +enum { /* UTF8_cvalN is value of masked first byte of N byte sequence */ + UTF8_cval1 = 0x00, + UTF8_cval2 = 0xc0, + UTF8_cval3 = 0xe0, + UTF8_cval4 = 0xf0 +}; + +void +_INTERNAL_trim_to_complete_utf8_characters(const char *from, + const char **fromLimRef) { + const char *fromLim = *fromLimRef; + size_t walked = 0; + for (; fromLim > from; fromLim--, walked++) { + const unsigned char prev = (unsigned char)fromLim[-1]; + if ((prev & 0xf8u) + == 0xf0u) { /* 4-byte character, lead by 0b11110xxx byte */ + if (walked + 1 >= 4) { + fromLim += 4 - 1; + break; + } else { + walked = 0; + } + } else if ((prev & 0xf0u) + == 0xe0u) { /* 3-byte character, lead by 0b1110xxxx byte */ + if (walked + 1 >= 3) { + fromLim += 3 - 1; + break; + } else { + walked = 0; + } + } else if ((prev & 0xe0u) + == 0xc0u) { /* 2-byte character, lead by 0b110xxxxx byte */ + if (walked + 1 >= 2) { + fromLim += 2 - 1; + break; + } else { + walked = 0; + } + } else if ((prev & 0x80u) + == 0x00u) { /* 1-byte character, matching 0b0xxxxxxx */ + break; + } + } + *fromLimRef = fromLim; +} + +static enum XML_Convert_Result +utf8_toUtf8(const ENCODING *enc, const char **fromP, const char *fromLim, + char **toP, const char *toLim) { + bool input_incomplete = false; + bool output_exhausted = false; + + /* Avoid copying partial characters (due to limited space). */ + const ptrdiff_t bytesAvailable = fromLim - *fromP; + const ptrdiff_t bytesStorable = toLim - *toP; + UNUSED_P(enc); + if (bytesAvailable > bytesStorable) { + fromLim = *fromP + bytesStorable; + output_exhausted = true; + } + + /* Avoid copying partial characters (from incomplete input). */ + { + const char *const fromLimBefore = fromLim; + _INTERNAL_trim_to_complete_utf8_characters(*fromP, &fromLim); + if (fromLim < fromLimBefore) { + input_incomplete = true; + } + } + + { + const ptrdiff_t bytesToCopy = fromLim - *fromP; + memcpy(*toP, *fromP, bytesToCopy); + *fromP += bytesToCopy; + *toP += bytesToCopy; + } + + if (output_exhausted) /* needs to go first */ + return XML_CONVERT_OUTPUT_EXHAUSTED; + else if (input_incomplete) + return XML_CONVERT_INPUT_INCOMPLETE; + else + return XML_CONVERT_COMPLETED; +} + +static enum XML_Convert_Result +utf8_toUtf16(const ENCODING *enc, const char **fromP, const char *fromLim, + unsigned short **toP, const unsigned short *toLim) { + enum XML_Convert_Result res = XML_CONVERT_COMPLETED; + unsigned short *to = *toP; + const char *from = *fromP; + while (from < fromLim && to < toLim) { + switch (SB_BYTE_TYPE(enc, from)) { + case BT_LEAD2: + if (fromLim - from < 2) { + res = XML_CONVERT_INPUT_INCOMPLETE; + goto after; + } + *to++ = (unsigned short)(((from[0] & 0x1f) << 6) | (from[1] & 0x3f)); + from += 2; + break; + case BT_LEAD3: + if (fromLim - from < 3) { + res = XML_CONVERT_INPUT_INCOMPLETE; + goto after; + } + *to++ = (unsigned short)(((from[0] & 0xf) << 12) | ((from[1] & 0x3f) << 6) + | (from[2] & 0x3f)); + from += 3; + break; + case BT_LEAD4: { + unsigned long n; + if (toLim - to < 2) { + res = XML_CONVERT_OUTPUT_EXHAUSTED; + goto after; + } + if (fromLim - from < 4) { + res = XML_CONVERT_INPUT_INCOMPLETE; + goto after; + } + n = ((from[0] & 0x7) << 18) | ((from[1] & 0x3f) << 12) + | ((from[2] & 0x3f) << 6) | (from[3] & 0x3f); + n -= 0x10000; + to[0] = (unsigned short)((n >> 10) | 0xD800); + to[1] = (unsigned short)((n & 0x3FF) | 0xDC00); + to += 2; + from += 4; + } break; + default: + *to++ = *from++; + break; + } + } + if (from < fromLim) + res = XML_CONVERT_OUTPUT_EXHAUSTED; +after: + *fromP = from; + *toP = to; + return res; +} + +#ifdef XML_NS +static const struct normal_encoding utf8_encoding_ns + = {{VTABLE1, utf8_toUtf8, utf8_toUtf16, 1, 1, 0}, + { +# include "asciitab.h" +# include "utf8tab.h" + }, + STANDARD_VTABLE(sb_) NORMAL_VTABLE(utf8_)}; +#endif + +static const struct normal_encoding utf8_encoding + = {{VTABLE1, utf8_toUtf8, utf8_toUtf16, 1, 1, 0}, + { +#define BT_COLON BT_NMSTRT +#include "asciitab.h" +#undef BT_COLON +#include "utf8tab.h" + }, + STANDARD_VTABLE(sb_) NORMAL_VTABLE(utf8_)}; + +#ifdef XML_NS + +static const struct normal_encoding internal_utf8_encoding_ns + = {{VTABLE1, utf8_toUtf8, utf8_toUtf16, 1, 1, 0}, + { +# include "iasciitab.h" +# include "utf8tab.h" + }, + STANDARD_VTABLE(sb_) NORMAL_VTABLE(utf8_)}; + +#endif + +static const struct normal_encoding internal_utf8_encoding + = {{VTABLE1, utf8_toUtf8, utf8_toUtf16, 1, 1, 0}, + { +#define BT_COLON BT_NMSTRT +#include "iasciitab.h" +#undef BT_COLON +#include "utf8tab.h" + }, + STANDARD_VTABLE(sb_) NORMAL_VTABLE(utf8_)}; + +static enum XML_Convert_Result +latin1_toUtf8(const ENCODING *enc, const char **fromP, const char *fromLim, + char **toP, const char *toLim) { + UNUSED_P(enc); + for (;;) { + unsigned char c; + if (*fromP == fromLim) + return XML_CONVERT_COMPLETED; + c = (unsigned char)**fromP; + if (c & 0x80) { + if (toLim - *toP < 2) + return XML_CONVERT_OUTPUT_EXHAUSTED; + *(*toP)++ = (char)((c >> 6) | UTF8_cval2); + *(*toP)++ = (char)((c & 0x3f) | 0x80); + (*fromP)++; + } else { + if (*toP == toLim) + return XML_CONVERT_OUTPUT_EXHAUSTED; + *(*toP)++ = *(*fromP)++; + } + } +} + +static enum XML_Convert_Result +latin1_toUtf16(const ENCODING *enc, const char **fromP, const char *fromLim, + unsigned short **toP, const unsigned short *toLim) { + UNUSED_P(enc); + while (*fromP < fromLim && *toP < toLim) + *(*toP)++ = (unsigned char)*(*fromP)++; + + if ((*toP == toLim) && (*fromP < fromLim)) + return XML_CONVERT_OUTPUT_EXHAUSTED; + else + return XML_CONVERT_COMPLETED; +} + +#ifdef XML_NS + +static const struct normal_encoding latin1_encoding_ns + = {{VTABLE1, latin1_toUtf8, latin1_toUtf16, 1, 0, 0}, + { +# include "asciitab.h" +# include "latin1tab.h" + }, + STANDARD_VTABLE(sb_) NULL_VTABLE}; + +#endif + +static const struct normal_encoding latin1_encoding + = {{VTABLE1, latin1_toUtf8, latin1_toUtf16, 1, 0, 0}, + { +#define BT_COLON BT_NMSTRT +#include "asciitab.h" +#undef BT_COLON +#include "latin1tab.h" + }, + STANDARD_VTABLE(sb_) NULL_VTABLE}; + +static enum XML_Convert_Result +ascii_toUtf8(const ENCODING *enc, const char **fromP, const char *fromLim, + char **toP, const char *toLim) { + UNUSED_P(enc); + while (*fromP < fromLim && *toP < toLim) + *(*toP)++ = *(*fromP)++; + + if ((*toP == toLim) && (*fromP < fromLim)) + return XML_CONVERT_OUTPUT_EXHAUSTED; + else + return XML_CONVERT_COMPLETED; +} + +#ifdef XML_NS + +static const struct normal_encoding ascii_encoding_ns + = {{VTABLE1, ascii_toUtf8, latin1_toUtf16, 1, 1, 0}, + { +# include "asciitab.h" + /* BT_NONXML == 0 */ + }, + STANDARD_VTABLE(sb_) NULL_VTABLE}; + +#endif + +static const struct normal_encoding ascii_encoding + = {{VTABLE1, ascii_toUtf8, latin1_toUtf16, 1, 1, 0}, + { +#define BT_COLON BT_NMSTRT +#include "asciitab.h" +#undef BT_COLON + /* BT_NONXML == 0 */ + }, + STANDARD_VTABLE(sb_) NULL_VTABLE}; + +static int +unicode_byte_type(char hi, char lo) { + switch ((unsigned char)hi) { + /* 0xD800-0xDBFF first 16-bit code unit or high surrogate (W1) */ + case 0xD8: + case 0xD9: + case 0xDA: + case 0xDB: + return BT_LEAD4; + /* 0xDC00-0xDFFF second 16-bit code unit or low surrogate (W2) */ + case 0xDC: + case 0xDD: + case 0xDE: + case 0xDF: + return BT_TRAIL; + case 0xFF: + switch ((unsigned char)lo) { + case 0xFF: /* noncharacter-FFFF */ + case 0xFE: /* noncharacter-FFFE */ + return BT_NONXML; + } + break; + } + return BT_NONASCII; +} + +#define DEFINE_UTF16_TO_UTF8(E) \ + static enum XML_Convert_Result E##toUtf8( \ + const ENCODING *enc, const char **fromP, const char *fromLim, \ + char **toP, const char *toLim) { \ + const char *from = *fromP; \ + UNUSED_P(enc); \ + fromLim = from + (((fromLim - from) >> 1) << 1); /* shrink to even */ \ + for (; from < fromLim; from += 2) { \ + int plane; \ + unsigned char lo2; \ + unsigned char lo = GET_LO(from); \ + unsigned char hi = GET_HI(from); \ + switch (hi) { \ + case 0: \ + if (lo < 0x80) { \ + if (*toP == toLim) { \ + *fromP = from; \ + return XML_CONVERT_OUTPUT_EXHAUSTED; \ + } \ + *(*toP)++ = lo; \ + break; \ + } \ + EXPAT_FALLTHROUGH; \ + case 0x1: \ + case 0x2: \ + case 0x3: \ + case 0x4: \ + case 0x5: \ + case 0x6: \ + case 0x7: \ + if (toLim - *toP < 2) { \ + *fromP = from; \ + return XML_CONVERT_OUTPUT_EXHAUSTED; \ + } \ + *(*toP)++ = ((lo >> 6) | (hi << 2) | UTF8_cval2); \ + *(*toP)++ = ((lo & 0x3f) | 0x80); \ + break; \ + default: \ + if (toLim - *toP < 3) { \ + *fromP = from; \ + return XML_CONVERT_OUTPUT_EXHAUSTED; \ + } \ + /* 16 bits divided 4, 6, 6 amongst 3 bytes */ \ + *(*toP)++ = ((hi >> 4) | UTF8_cval3); \ + *(*toP)++ = (((hi & 0xf) << 2) | (lo >> 6) | 0x80); \ + *(*toP)++ = ((lo & 0x3f) | 0x80); \ + break; \ + case 0xD8: \ + case 0xD9: \ + case 0xDA: \ + case 0xDB: \ + if (toLim - *toP < 4) { \ + *fromP = from; \ + return XML_CONVERT_OUTPUT_EXHAUSTED; \ + } \ + if (fromLim - from < 4) { \ + *fromP = from; \ + return XML_CONVERT_INPUT_INCOMPLETE; \ + } \ + plane = (((hi & 0x3) << 2) | ((lo >> 6) & 0x3)) + 1; \ + *(*toP)++ = (char)((plane >> 2) | UTF8_cval4); \ + *(*toP)++ = (((lo >> 2) & 0xF) | ((plane & 0x3) << 4) | 0x80); \ + from += 2; \ + lo2 = GET_LO(from); \ + *(*toP)++ = (((lo & 0x3) << 4) | ((GET_HI(from) & 0x3) << 2) \ + | (lo2 >> 6) | 0x80); \ + *(*toP)++ = ((lo2 & 0x3f) | 0x80); \ + break; \ + } \ + } \ + *fromP = from; \ + if (from < fromLim) \ + return XML_CONVERT_INPUT_INCOMPLETE; \ + else \ + return XML_CONVERT_COMPLETED; \ + } + +#define DEFINE_UTF16_TO_UTF16(E) \ + static enum XML_Convert_Result E##toUtf16( \ + const ENCODING *enc, const char **fromP, const char *fromLim, \ + unsigned short **toP, const unsigned short *toLim) { \ + enum XML_Convert_Result res = XML_CONVERT_COMPLETED; \ + UNUSED_P(enc); \ + fromLim = *fromP + (((fromLim - *fromP) >> 1) << 1); /* shrink to even */ \ + /* Avoid copying the first half (2 bytes) of surrogate pairs (4 bytes) */ \ + if (fromLim - *fromP > ((toLim - *toP) << 1) \ + && /* are the last two bytes a high surrogate (0xD800-0xDBFF)? */ \ + (GET_HI(fromLim - 2) & 0xFC) == 0xD8) { \ + fromLim -= 2; \ + res = XML_CONVERT_INPUT_INCOMPLETE; \ + } \ + for (; *fromP < fromLim && *toP < toLim; *fromP += 2) \ + *(*toP)++ = (GET_HI(*fromP) << 8) | GET_LO(*fromP); \ + if ((*toP == toLim) && (*fromP < fromLim)) \ + return XML_CONVERT_OUTPUT_EXHAUSTED; \ + else \ + return res; \ + } + +#define GET_LO(ptr) ((unsigned char)(ptr)[0]) +#define GET_HI(ptr) ((unsigned char)(ptr)[1]) + +DEFINE_UTF16_TO_UTF8(little2_) +DEFINE_UTF16_TO_UTF16(little2_) + +#undef GET_LO +#undef GET_HI + +#define GET_LO(ptr) ((unsigned char)(ptr)[1]) +#define GET_HI(ptr) ((unsigned char)(ptr)[0]) + +DEFINE_UTF16_TO_UTF8(big2_) +DEFINE_UTF16_TO_UTF16(big2_) + +#undef GET_LO +#undef GET_HI + +#define LITTLE2_BYTE_TYPE(enc, p) \ + ((p)[1] == 0 ? SB_BYTE_TYPE(enc, p) : unicode_byte_type((p)[1], (p)[0])) +#define LITTLE2_BYTE_TO_ASCII(p) ((p)[1] == 0 ? (p)[0] : -1) +#define LITTLE2_CHAR_MATCHES(p, c) ((p)[1] == 0 && (p)[0] == (c)) +#define LITTLE2_IS_NAME_CHAR_MINBPC(p) \ + UCS2_GET_NAMING(namePages, (unsigned char)p[1], (unsigned char)p[0]) +#define LITTLE2_IS_NMSTRT_CHAR_MINBPC(p) \ + UCS2_GET_NAMING(nmstrtPages, (unsigned char)p[1], (unsigned char)p[0]) +/* A 4-byte UTF-16 character is a surrogate pair; byteType only reports BT_LEAD4 + for a high surrogate, so the pair is invalid unless the second unit is a low + surrogate (U+DC00..U+DFFF, i.e. high byte 0xDC..0xDF). */ +#define LITTLE2_IS_INVALID_CHAR(p, n) \ + ((n) == 4 && ((unsigned char)(p)[3] & 0xFC) != 0xDC) + +#ifdef XML_MIN_SIZE + +static int +little2_byteType(const ENCODING *enc, const char *p) { + return LITTLE2_BYTE_TYPE(enc, p); +} + +static int +little2_byteToAscii(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return LITTLE2_BYTE_TO_ASCII(p); +} + +static int +little2_charMatches(const ENCODING *enc, const char *p, int c) { + UNUSED_P(enc); + return LITTLE2_CHAR_MATCHES(p, c); +} + +static int +little2_isNameMin(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return LITTLE2_IS_NAME_CHAR_MINBPC(p); +} + +static int +little2_isNmstrtMin(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return LITTLE2_IS_NMSTRT_CHAR_MINBPC(p); +} + +static int +little2_isInvalid4(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return LITTLE2_IS_INVALID_CHAR(p, 4); +} + +# undef VTABLE +# define VTABLE VTABLE1, little2_toUtf8, little2_toUtf16 + +#else /* not XML_MIN_SIZE */ + +# undef PREFIX +# define PREFIX(ident) little2_##ident +# define MINBPC(enc) 2 +/* CHAR_MATCHES is guaranteed to have MINBPC bytes available. */ +# define BYTE_TYPE(enc, p) LITTLE2_BYTE_TYPE(enc, p) +# define BYTE_TO_ASCII(enc, p) LITTLE2_BYTE_TO_ASCII(p) +# define CHAR_MATCHES(enc, p, c) LITTLE2_CHAR_MATCHES(p, c) +# define IS_NAME_CHAR(enc, p, n) 0 +# define IS_NAME_CHAR_MINBPC(enc, p) LITTLE2_IS_NAME_CHAR_MINBPC(p) +# define IS_NMSTRT_CHAR(enc, p, n) (0) +# define IS_NMSTRT_CHAR_MINBPC(enc, p) LITTLE2_IS_NMSTRT_CHAR_MINBPC(p) +# define IS_INVALID_CHAR(enc, p, n) LITTLE2_IS_INVALID_CHAR(p, n) + +# define XML_TOK_IMPL_C +# include "xmltok_impl.c" +# undef XML_TOK_IMPL_C + +# undef MINBPC +# undef BYTE_TYPE +# undef BYTE_TO_ASCII +# undef CHAR_MATCHES +# undef IS_NAME_CHAR +# undef IS_NAME_CHAR_MINBPC +# undef IS_NMSTRT_CHAR +# undef IS_NMSTRT_CHAR_MINBPC +# undef IS_INVALID_CHAR + +#endif /* not XML_MIN_SIZE */ + +#ifdef XML_NS + +static const struct normal_encoding little2_encoding_ns + = {{VTABLE, 2, 0, +# if BYTEORDER == 1234 + 1 +# else + 0 +# endif + }, + { +# include "asciitab.h" +# include "latin1tab.h" + }, + STANDARD_VTABLE(little2_) UTF16_NULL_VTABLE(little2_)}; + +#endif + +static const struct normal_encoding little2_encoding + = {{VTABLE, 2, 0, +#if BYTEORDER == 1234 + 1 +#else + 0 +#endif + }, + { +#define BT_COLON BT_NMSTRT +#include "asciitab.h" +#undef BT_COLON +#include "latin1tab.h" + }, + STANDARD_VTABLE(little2_) UTF16_NULL_VTABLE(little2_)}; + +#if BYTEORDER != 4321 + +# ifdef XML_NS + +static const struct normal_encoding internal_little2_encoding_ns + = {{VTABLE, 2, 0, 1}, + { +# include "iasciitab.h" +# include "latin1tab.h" + }, + STANDARD_VTABLE(little2_) UTF16_NULL_VTABLE(little2_)}; + +# endif + +static const struct normal_encoding internal_little2_encoding + = {{VTABLE, 2, 0, 1}, + { +# define BT_COLON BT_NMSTRT +# include "iasciitab.h" +# undef BT_COLON +# include "latin1tab.h" + }, + STANDARD_VTABLE(little2_) UTF16_NULL_VTABLE(little2_)}; + +#endif + +#define BIG2_BYTE_TYPE(enc, p) \ + ((p)[0] == 0 ? SB_BYTE_TYPE(enc, p + 1) : unicode_byte_type((p)[0], (p)[1])) +#define BIG2_BYTE_TO_ASCII(p) ((p)[0] == 0 ? (p)[1] : -1) +#define BIG2_CHAR_MATCHES(p, c) ((p)[0] == 0 && (p)[1] == (c)) +#define BIG2_IS_NAME_CHAR_MINBPC(p) \ + UCS2_GET_NAMING(namePages, (unsigned char)p[0], (unsigned char)p[1]) +#define BIG2_IS_NMSTRT_CHAR_MINBPC(p) \ + UCS2_GET_NAMING(nmstrtPages, (unsigned char)p[0], (unsigned char)p[1]) +/* A 4-byte UTF-16 character is a surrogate pair; byteType only reports BT_LEAD4 + for a high surrogate, so the pair is invalid unless the second unit is a low + surrogate (U+DC00..U+DFFF, i.e. high byte 0xDC..0xDF). */ +#define BIG2_IS_INVALID_CHAR(p, n) \ + ((n) == 4 && ((unsigned char)(p)[2] & 0xFC) != 0xDC) + +#ifdef XML_MIN_SIZE + +static int +big2_byteType(const ENCODING *enc, const char *p) { + return BIG2_BYTE_TYPE(enc, p); +} + +static int +big2_byteToAscii(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return BIG2_BYTE_TO_ASCII(p); +} + +static int +big2_charMatches(const ENCODING *enc, const char *p, int c) { + UNUSED_P(enc); + return BIG2_CHAR_MATCHES(p, c); +} + +static int +big2_isNameMin(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return BIG2_IS_NAME_CHAR_MINBPC(p); +} + +static int +big2_isNmstrtMin(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return BIG2_IS_NMSTRT_CHAR_MINBPC(p); +} + +static int +big2_isInvalid4(const ENCODING *enc, const char *p) { + UNUSED_P(enc); + return BIG2_IS_INVALID_CHAR(p, 4); +} + +# undef VTABLE +# define VTABLE VTABLE1, big2_toUtf8, big2_toUtf16 + +#else /* not XML_MIN_SIZE */ + +# undef PREFIX +# define PREFIX(ident) big2_##ident +# define MINBPC(enc) 2 +/* CHAR_MATCHES is guaranteed to have MINBPC bytes available. */ +# define BYTE_TYPE(enc, p) BIG2_BYTE_TYPE(enc, p) +# define BYTE_TO_ASCII(enc, p) BIG2_BYTE_TO_ASCII(p) +# define CHAR_MATCHES(enc, p, c) BIG2_CHAR_MATCHES(p, c) +# define IS_NAME_CHAR(enc, p, n) 0 +# define IS_NAME_CHAR_MINBPC(enc, p) BIG2_IS_NAME_CHAR_MINBPC(p) +# define IS_NMSTRT_CHAR(enc, p, n) (0) +# define IS_NMSTRT_CHAR_MINBPC(enc, p) BIG2_IS_NMSTRT_CHAR_MINBPC(p) +# define IS_INVALID_CHAR(enc, p, n) BIG2_IS_INVALID_CHAR(p, n) + +# define XML_TOK_IMPL_C +# include "xmltok_impl.c" +# undef XML_TOK_IMPL_C + +# undef MINBPC +# undef BYTE_TYPE +# undef BYTE_TO_ASCII +# undef CHAR_MATCHES +# undef IS_NAME_CHAR +# undef IS_NAME_CHAR_MINBPC +# undef IS_NMSTRT_CHAR +# undef IS_NMSTRT_CHAR_MINBPC +# undef IS_INVALID_CHAR + +#endif /* not XML_MIN_SIZE */ + +#ifdef XML_NS + +static const struct normal_encoding big2_encoding_ns + = {{VTABLE, 2, 0, +# if BYTEORDER == 4321 + 1 +# else + 0 +# endif + }, + { +# include "asciitab.h" +# include "latin1tab.h" + }, + STANDARD_VTABLE(big2_) UTF16_NULL_VTABLE(big2_)}; + +#endif + +static const struct normal_encoding big2_encoding + = {{VTABLE, 2, 0, +#if BYTEORDER == 4321 + 1 +#else + 0 +#endif + }, + { +#define BT_COLON BT_NMSTRT +#include "asciitab.h" +#undef BT_COLON +#include "latin1tab.h" + }, + STANDARD_VTABLE(big2_) UTF16_NULL_VTABLE(big2_)}; + +#if BYTEORDER != 1234 + +# ifdef XML_NS + +static const struct normal_encoding internal_big2_encoding_ns + = {{VTABLE, 2, 0, 1}, + { +# include "iasciitab.h" +# include "latin1tab.h" + }, + STANDARD_VTABLE(big2_) UTF16_NULL_VTABLE(big2_)}; + +# endif + +static const struct normal_encoding internal_big2_encoding + = {{VTABLE, 2, 0, 1}, + { +# define BT_COLON BT_NMSTRT +# include "iasciitab.h" +# undef BT_COLON +# include "latin1tab.h" + }, + STANDARD_VTABLE(big2_) UTF16_NULL_VTABLE(big2_)}; + +#endif + +#undef PREFIX + +static int +streqci(const char *s1, const char *s2) { + for (;;) { + char c1 = *s1++; + char c2 = *s2++; + if (ASCII_a <= c1 && c1 <= ASCII_z) + c1 += ASCII_A - ASCII_a; + if (ASCII_a <= c2 && c2 <= ASCII_z) + /* The following line will never get executed. streqci() is + * only called from two places, both of which guarantee to put + * upper-case strings into s2. + */ + c2 += ASCII_A - ASCII_a; /* LCOV_EXCL_LINE */ + if (c1 != c2) + return 0; + if (! c1) + break; + } + return 1; +} + +static void +initUpdatePosition(const ENCODING *enc, const char *ptr, const char *end, + POSITION *pos) { + UNUSED_P(enc); + normal_updatePosition(&utf8_encoding.enc, ptr, end, pos); +} + +static int +toAscii(const ENCODING *enc, const char *ptr, const char *end) { + char buf[1]; + char *p = buf; + XmlUtf8Convert(enc, &ptr, end, &p, p + 1); + if (p == buf) + return -1; + else + return buf[0]; +} + +static int +isSpace(int c) { + switch (c) { + case 0x20: + case 0xD: + case 0xA: + case 0x9: + return 1; + } + return 0; +} + +/* Return 1 if there's just optional white space or there's an S + followed by name=val. +*/ +static int +parsePseudoAttribute(const ENCODING *enc, const char *ptr, const char *end, + const char **namePtr, const char **nameEndPtr, + const char **valPtr, const char **nextTokPtr) { + int c; + char open; + if (ptr == end) { + *namePtr = NULL; + return 1; + } + if (! isSpace(toAscii(enc, ptr, end))) { + *nextTokPtr = ptr; + return 0; + } + do { + ptr += enc->minBytesPerChar; + } while (isSpace(toAscii(enc, ptr, end))); + if (ptr == end) { + *namePtr = NULL; + return 1; + } + *namePtr = ptr; + for (;;) { + c = toAscii(enc, ptr, end); + if (c == -1) { + *nextTokPtr = ptr; + return 0; + } + if (c == ASCII_EQUALS) { + *nameEndPtr = ptr; + break; + } + if (isSpace(c)) { + *nameEndPtr = ptr; + do { + ptr += enc->minBytesPerChar; + } while (isSpace(c = toAscii(enc, ptr, end))); + if (c != ASCII_EQUALS) { + *nextTokPtr = ptr; + return 0; + } + break; + } + ptr += enc->minBytesPerChar; + } + if (ptr == *namePtr) { + *nextTokPtr = ptr; + return 0; + } + ptr += enc->minBytesPerChar; + c = toAscii(enc, ptr, end); + while (isSpace(c)) { + ptr += enc->minBytesPerChar; + c = toAscii(enc, ptr, end); + } + if (c != ASCII_QUOT && c != ASCII_APOS) { + *nextTokPtr = ptr; + return 0; + } + open = (char)c; + ptr += enc->minBytesPerChar; + *valPtr = ptr; + for (;; ptr += enc->minBytesPerChar) { + c = toAscii(enc, ptr, end); + if (c == open) + break; + if (! (ASCII_a <= c && c <= ASCII_z) && ! (ASCII_A <= c && c <= ASCII_Z) + && ! (ASCII_0 <= c && c <= ASCII_9) && c != ASCII_PERIOD + && c != ASCII_MINUS && c != ASCII_UNDERSCORE) { + *nextTokPtr = ptr; + return 0; + } + } + *nextTokPtr = ptr + enc->minBytesPerChar; + return 1; +} + +static const char KW_version[] + = {ASCII_v, ASCII_e, ASCII_r, ASCII_s, ASCII_i, ASCII_o, ASCII_n, '\0'}; + +static const char KW_encoding[] = {ASCII_e, ASCII_n, ASCII_c, ASCII_o, ASCII_d, + ASCII_i, ASCII_n, ASCII_g, '\0'}; + +static const char KW_standalone[] + = {ASCII_s, ASCII_t, ASCII_a, ASCII_n, ASCII_d, ASCII_a, + ASCII_l, ASCII_o, ASCII_n, ASCII_e, '\0'}; + +static const char KW_yes[] = {ASCII_y, ASCII_e, ASCII_s, '\0'}; + +static const char KW_no[] = {ASCII_n, ASCII_o, '\0'}; + +static const char KW_1_dot[] = {ASCII_1, ASCII_PERIOD, '\0'}; + +/* Checks a version pseudo-attribute value against the VersionNum production. + XML 1.0 Fourth Edition only allows the literal "1.0", but the Fifth + Edition relaxed this to "1." followed by one or more digits, since Expat + only implements 1.0 itself but plans to track the Fifth Edition's laxer + grammar here so that "1.1" and similar aren't rejected only to have that + rejection reverted later. Returns true for a value matching "1.[0-9]+". + val/valEnd bound the value itself; valEnd is the upper bound used when + decoding the individual characters between them. */ +static bool +checkXmlDeclVersionNum(const ENCODING *enc, const char *val, + const char *valEnd) { + if (valEnd - val < 2 * enc->minBytesPerChar + || ! XmlNameMatchesAscii(enc, val, val + 2 * enc->minBytesPerChar, + KW_1_dot)) + return false; + val += 2 * enc->minBytesPerChar; + if (val == valEnd) + return false; + for (; val != valEnd; val += enc->minBytesPerChar) { + int c = toAscii(enc, val, valEnd); + if (c < ASCII_0 || c > ASCII_9) + return false; + } + return true; +} + +static int +doParseXmlDecl(const ENCODING *(*encodingFinder)(const ENCODING *, const char *, + const char *), + int isGeneralTextEntity, const ENCODING *enc, const char *ptr, + const char *end, const char **badPtr, const char **versionPtr, + const char **versionEndPtr, const char **encodingName, + const ENCODING **encoding, int *standalone) { + const char *val = NULL; + const char *name = NULL; + const char *nameEnd = NULL; + ptr += 5 * enc->minBytesPerChar; + end -= 2 * enc->minBytesPerChar; + if (! parsePseudoAttribute(enc, ptr, end, &name, &nameEnd, &val, &ptr) + || ! name) { + *badPtr = ptr; + return 0; + } + if (! XmlNameMatchesAscii(enc, name, nameEnd, KW_version)) { + if (! isGeneralTextEntity) { + *badPtr = name; + return 0; + } + } else { + if (versionPtr) + *versionPtr = val; + if (versionEndPtr) + *versionEndPtr = ptr; + /* The version number must not be empty; VersionNum requires at least + one character. The encoding and standalone pseudo-attributes below + already reject an empty value, so keep version consistent. */ + if (val == ptr - enc->minBytesPerChar) { + *badPtr = val; + return 0; + } + /* Expat implements XML 1.0 only, so any version outside the "1.0"/"1.x" + family is rejected. Following the Fifth Edition's VersionNum + production (rather than the Fourth Edition's exact "1.0") avoids + rejecting "1.1" now only to have to revert that once Expat tracks + the newer edition. */ + if (! checkXmlDeclVersionNum(enc, val, ptr - enc->minBytesPerChar)) { + *badPtr = val; + return 0; + } + if (! parsePseudoAttribute(enc, ptr, end, &name, &nameEnd, &val, &ptr)) { + *badPtr = ptr; + return 0; + } + if (! name) { + if (isGeneralTextEntity) { + /* a TextDecl must have an EncodingDecl */ + *badPtr = ptr; + return 0; + } + return 1; + } + } + if (XmlNameMatchesAscii(enc, name, nameEnd, KW_encoding)) { + int c = toAscii(enc, val, end); + if (! (ASCII_a <= c && c <= ASCII_z) && ! (ASCII_A <= c && c <= ASCII_Z)) { + *badPtr = val; + return 0; + } + if (encodingName) + *encodingName = val; + if (encoding) + *encoding = encodingFinder(enc, val, ptr - enc->minBytesPerChar); + if (! parsePseudoAttribute(enc, ptr, end, &name, &nameEnd, &val, &ptr)) { + *badPtr = ptr; + return 0; + } + if (! name) + return 1; + } + if (! XmlNameMatchesAscii(enc, name, nameEnd, KW_standalone) + || isGeneralTextEntity) { + *badPtr = name; + return 0; + } + if (XmlNameMatchesAscii(enc, val, ptr - enc->minBytesPerChar, KW_yes)) { + if (standalone) + *standalone = 1; + } else if (XmlNameMatchesAscii(enc, val, ptr - enc->minBytesPerChar, KW_no)) { + if (standalone) + *standalone = 0; + } else { + *badPtr = val; + return 0; + } + while (isSpace(toAscii(enc, ptr, end))) + ptr += enc->minBytesPerChar; + if (ptr != end) { + *badPtr = ptr; + return 0; + } + return 1; +} + +static int +checkCharRefNumber(int result) { + switch (result >> 8) { + case 0xD8: + case 0xD9: + case 0xDA: + case 0xDB: + case 0xDC: + case 0xDD: + case 0xDE: + case 0xDF: + return -1; + case 0: + if (latin1_encoding.type[result] == BT_NONXML) + return -1; + break; + case 0xFF: + if (result == 0xFFFE || result == 0xFFFF) + return -1; + break; + } + return result; +} + +int +XmlUtf8Encode(int c, char *buf) { + enum { + /* minN is minimum legal resulting value for N byte sequence */ + min2 = 0x80, + min3 = 0x800, + min4 = 0x10000 + }; + + if (c < 0) + return 0; /* LCOV_EXCL_LINE: this case is always eliminated beforehand */ + if (c < min2) { + buf[0] = (char)(c | UTF8_cval1); + return 1; + } + if (c < min3) { + buf[0] = (char)((c >> 6) | UTF8_cval2); + buf[1] = (char)((c & 0x3f) | 0x80); + return 2; + } + if (c < min4) { + buf[0] = (char)((c >> 12) | UTF8_cval3); + buf[1] = (char)(((c >> 6) & 0x3f) | 0x80); + buf[2] = (char)((c & 0x3f) | 0x80); + return 3; + } + if (c < 0x110000) { + buf[0] = (char)((c >> 18) | UTF8_cval4); + buf[1] = (char)(((c >> 12) & 0x3f) | 0x80); + buf[2] = (char)(((c >> 6) & 0x3f) | 0x80); + buf[3] = (char)((c & 0x3f) | 0x80); + return 4; + } + return 0; /* LCOV_EXCL_LINE: this case too is eliminated before calling */ +} + +int +XmlUtf16Encode(int charNum, unsigned short *buf) { + if (charNum < 0) + return 0; + if (charNum < 0x10000) { + buf[0] = (unsigned short)charNum; + return 1; + } + if (charNum < 0x110000) { + charNum -= 0x10000; + buf[0] = (unsigned short)((charNum >> 10) + 0xD800); + buf[1] = (unsigned short)((charNum & 0x3FF) + 0xDC00); + return 2; + } + return 0; +} + +struct unknown_encoding { + struct normal_encoding normal; + CONVERTER convert; + void *userData; + unsigned short utf16[256]; + char utf8[256][4]; +}; + +#define AS_UNKNOWN_ENCODING(enc) ((const struct unknown_encoding *)(enc)) + +int +XmlSizeOfUnknownEncoding(void) { + return sizeof(struct unknown_encoding); +} + +static int +unknown_isName(const ENCODING *enc, const char *p) { + const struct unknown_encoding *uenc = AS_UNKNOWN_ENCODING(enc); + int c = uenc->convert(uenc->userData, p); + if (c & ~0xFFFF) + return 0; + return UCS2_GET_NAMING(namePages, c >> 8, c & 0xFF); +} + +static int +unknown_isNmstrt(const ENCODING *enc, const char *p) { + const struct unknown_encoding *uenc = AS_UNKNOWN_ENCODING(enc); + int c = uenc->convert(uenc->userData, p); + if (c & ~0xFFFF) + return 0; + return UCS2_GET_NAMING(nmstrtPages, c >> 8, c & 0xFF); +} + +static int +unknown_isInvalid(const ENCODING *enc, const char *p) { + const struct unknown_encoding *uenc = AS_UNKNOWN_ENCODING(enc); + int c = uenc->convert(uenc->userData, p); + return (c & ~0xFFFF) || checkCharRefNumber(c) < 0; +} + +static enum XML_Convert_Result +unknown_toUtf8(const ENCODING *enc, const char **fromP, const char *fromLim, + char **toP, const char *toLim) { + const struct unknown_encoding *uenc = AS_UNKNOWN_ENCODING(enc); + char buf[XML_UTF8_ENCODE_MAX]; + for (;;) { + const char *utf8; + int n; + if (*fromP == fromLim) + return XML_CONVERT_COMPLETED; + utf8 = uenc->utf8[(unsigned char)**fromP]; + n = *utf8++; + if (n == 0) { + int c = uenc->convert(uenc->userData, *fromP); + n = XmlUtf8Encode(c, buf); + if (n > toLim - *toP) + return XML_CONVERT_OUTPUT_EXHAUSTED; + utf8 = buf; + *fromP += (AS_NORMAL_ENCODING(enc)->type[(unsigned char)**fromP] + - (BT_LEAD2 - 2)); + } else { + if (n > toLim - *toP) + return XML_CONVERT_OUTPUT_EXHAUSTED; + (*fromP)++; + } + memcpy(*toP, utf8, n); + *toP += n; + } +} + +static enum XML_Convert_Result +unknown_toUtf16(const ENCODING *enc, const char **fromP, const char *fromLim, + unsigned short **toP, const unsigned short *toLim) { + const struct unknown_encoding *uenc = AS_UNKNOWN_ENCODING(enc); + while (*fromP < fromLim && *toP < toLim) { + unsigned short c = uenc->utf16[(unsigned char)**fromP]; + if (c == 0) { + c = (unsigned short)uenc->convert(uenc->userData, *fromP); + *fromP += (AS_NORMAL_ENCODING(enc)->type[(unsigned char)**fromP] + - (BT_LEAD2 - 2)); + } else + (*fromP)++; + *(*toP)++ = c; + } + + if ((*toP == toLim) && (*fromP < fromLim)) + return XML_CONVERT_OUTPUT_EXHAUSTED; + else + return XML_CONVERT_COMPLETED; +} + +ENCODING * +XmlInitUnknownEncoding(void *mem, const int *table, CONVERTER convert, + void *userData) { + int i; + struct unknown_encoding *e = (struct unknown_encoding *)mem; + memcpy(mem, &latin1_encoding, sizeof(struct normal_encoding)); + for (i = 0; i < 128; i++) + if (latin1_encoding.type[i] != BT_OTHER + && latin1_encoding.type[i] != BT_NONXML && table[i] != i) + return 0; + for (i = 0; i < 256; i++) { + int c = table[i]; + if (c == -1) { + e->normal.type[i] = BT_MALFORM; + /* This shouldn't really get used. */ + e->utf16[i] = 0xFFFF; + e->utf8[i][0] = 1; + e->utf8[i][1] = 0; + } else if (c < 0) { + if (c < -4) + return 0; + /* Multi-byte sequences need a converter function */ + if (! convert) + return 0; + e->normal.type[i] = (unsigned char)(BT_LEAD2 - (c + 2)); + e->utf8[i][0] = 0; + e->utf16[i] = 0; + } else if (c < 0x80) { + if (latin1_encoding.type[c] != BT_OTHER + && latin1_encoding.type[c] != BT_NONXML && c != i) + return 0; + e->normal.type[i] = latin1_encoding.type[c]; + e->utf8[i][0] = 1; + e->utf8[i][1] = (char)c; + e->utf16[i] = (unsigned short)(c == 0 ? 0xFFFF : c); + } else if (checkCharRefNumber(c) < 0) { + e->normal.type[i] = BT_NONXML; + /* This shouldn't really get used. */ + e->utf16[i] = 0xFFFF; + e->utf8[i][0] = 1; + e->utf8[i][1] = 0; + } else { + if (c > 0xFFFF) + return 0; + if (UCS2_GET_NAMING(nmstrtPages, c >> 8, c & 0xff)) + e->normal.type[i] = BT_NMSTRT; + else if (UCS2_GET_NAMING(namePages, c >> 8, c & 0xff)) + e->normal.type[i] = BT_NAME; + else + e->normal.type[i] = BT_OTHER; + e->utf8[i][0] = (char)XmlUtf8Encode(c, e->utf8[i] + 1); + e->utf16[i] = (unsigned short)c; + } + } + e->userData = userData; + e->convert = convert; + if (convert) { + e->normal.isName2 = unknown_isName; + e->normal.isName3 = unknown_isName; + e->normal.isName4 = unknown_isName; + e->normal.isNmstrt2 = unknown_isNmstrt; + e->normal.isNmstrt3 = unknown_isNmstrt; + e->normal.isNmstrt4 = unknown_isNmstrt; + e->normal.isInvalid2 = unknown_isInvalid; + e->normal.isInvalid3 = unknown_isInvalid; + e->normal.isInvalid4 = unknown_isInvalid; + } + e->normal.enc.utf8Convert = unknown_toUtf8; + e->normal.enc.utf16Convert = unknown_toUtf16; + return &(e->normal.enc); +} + +/* If this enumeration is changed, getEncodingIndex and encodings +must also be changed. */ +enum { + UNKNOWN_ENC = -1, + ISO_8859_1_ENC = 0, + US_ASCII_ENC, + UTF_8_ENC, + UTF_16_ENC, + UTF_16BE_ENC, + UTF_16LE_ENC, + /* must match encodingNames up to here */ + NO_ENC +}; + +static const char KW_ISO_8859_1[] + = {ASCII_I, ASCII_S, ASCII_O, ASCII_MINUS, ASCII_8, ASCII_8, + ASCII_5, ASCII_9, ASCII_MINUS, ASCII_1, '\0'}; +static const char KW_US_ASCII[] + = {ASCII_U, ASCII_S, ASCII_MINUS, ASCII_A, ASCII_S, + ASCII_C, ASCII_I, ASCII_I, '\0'}; +static const char KW_UTF_8[] + = {ASCII_U, ASCII_T, ASCII_F, ASCII_MINUS, ASCII_8, '\0'}; +static const char KW_UTF_16[] + = {ASCII_U, ASCII_T, ASCII_F, ASCII_MINUS, ASCII_1, ASCII_6, '\0'}; +static const char KW_UTF_16BE[] + = {ASCII_U, ASCII_T, ASCII_F, ASCII_MINUS, ASCII_1, + ASCII_6, ASCII_B, ASCII_E, '\0'}; +static const char KW_UTF_16LE[] + = {ASCII_U, ASCII_T, ASCII_F, ASCII_MINUS, ASCII_1, + ASCII_6, ASCII_L, ASCII_E, '\0'}; + +static int +getEncodingIndex(const char *name) { + static const char *const encodingNames[] = { + KW_ISO_8859_1, KW_US_ASCII, KW_UTF_8, KW_UTF_16, KW_UTF_16BE, KW_UTF_16LE, + }; + int i; + if (name == NULL) + return NO_ENC; + for (i = 0; i < (int)(sizeof(encodingNames) / sizeof(encodingNames[0])); i++) + if (streqci(name, encodingNames[i])) + return i; + return UNKNOWN_ENC; +} + +/* For binary compatibility, we store the index of the encoding + specified at initialization in the isUtf16 member. +*/ + +#define INIT_ENC_INDEX(enc) ((int)(enc)->initEnc.isUtf16) +#define SET_INIT_ENC_INDEX(enc, i) ((enc)->initEnc.isUtf16 = (char)i) + +/* This is what detects the encoding. encodingTable maps from + encoding indices to encodings; INIT_ENC_INDEX(enc) is the index of + the external (protocol) specified encoding; state is + XML_CONTENT_STATE if we're parsing an external text entity, and + XML_PROLOG_STATE otherwise. +*/ + +static int +initScan(const ENCODING *const *encodingTable, const INIT_ENCODING *enc, + int state, const char *ptr, const char *end, const char **nextTokPtr) { + const ENCODING **encPtr; + + if (ptr >= end) + return XML_TOK_NONE; + encPtr = enc->encPtr; + if (ptr + 1 == end) { + /* only a single byte available for auto-detection */ +#ifndef XML_DTD /* FIXME */ + /* a well-formed document entity must have more than one byte */ + if (state != XML_CONTENT_STATE) + return XML_TOK_PARTIAL; +#endif + /* so we're parsing an external text entity... */ + /* if UTF-16 was externally specified, then we need at least 2 bytes */ + switch (INIT_ENC_INDEX(enc)) { + case UTF_16_ENC: + case UTF_16LE_ENC: + case UTF_16BE_ENC: + return XML_TOK_PARTIAL; + } + switch ((unsigned char)*ptr) { + case 0xFE: + case 0xFF: + case 0xEF: /* possibly first byte of UTF-8 BOM */ + if (INIT_ENC_INDEX(enc) == ISO_8859_1_ENC && state == XML_CONTENT_STATE) + break; + EXPAT_FALLTHROUGH; + case 0x00: + case 0x3C: + return XML_TOK_PARTIAL; + } + } else { + switch (((unsigned char)ptr[0] << 8) | (unsigned char)ptr[1]) { + case 0xFEFF: + if (INIT_ENC_INDEX(enc) == ISO_8859_1_ENC && state == XML_CONTENT_STATE) + break; + *nextTokPtr = ptr + 2; + *encPtr = encodingTable[UTF_16BE_ENC]; + return XML_TOK_BOM; + /* 00 3C is handled in the default case */ + case 0x3C00: + if ((INIT_ENC_INDEX(enc) == UTF_16BE_ENC + || INIT_ENC_INDEX(enc) == UTF_16_ENC) + && state == XML_CONTENT_STATE) + break; + *encPtr = encodingTable[UTF_16LE_ENC]; + return XmlTok(*encPtr, state, ptr, end, nextTokPtr); + case 0xFFFE: + if (INIT_ENC_INDEX(enc) == ISO_8859_1_ENC && state == XML_CONTENT_STATE) + break; + *nextTokPtr = ptr + 2; + *encPtr = encodingTable[UTF_16LE_ENC]; + return XML_TOK_BOM; + case 0xEFBB: + /* Maybe a UTF-8 BOM (EF BB BF) */ + /* If there's an explicitly specified (external) encoding + of ISO-8859-1 or some flavour of UTF-16 + and this is an external text entity, + don't look for the BOM, + because it might be a legal data. + */ + if (state == XML_CONTENT_STATE) { + int e = INIT_ENC_INDEX(enc); + if (e == ISO_8859_1_ENC || e == UTF_16BE_ENC || e == UTF_16LE_ENC + || e == UTF_16_ENC) + break; + } + if (ptr + 2 == end) + return XML_TOK_PARTIAL; + if ((unsigned char)ptr[2] == 0xBF) { + *nextTokPtr = ptr + 3; + *encPtr = encodingTable[UTF_8_ENC]; + return XML_TOK_BOM; + } + break; + default: + if (ptr[0] == '\0') { + /* 0 isn't a legal data character. Furthermore a document + entity can only start with ASCII characters. So the only + way this can fail to be big-endian UTF-16 if it it's an + external parsed general entity that's labelled as + UTF-16LE. + */ + if (state == XML_CONTENT_STATE && INIT_ENC_INDEX(enc) == UTF_16LE_ENC) + break; + *encPtr = encodingTable[UTF_16BE_ENC]; + return XmlTok(*encPtr, state, ptr, end, nextTokPtr); + } else if (ptr[1] == '\0') { + /* We could recover here in the case: + - parsing an external entity + - second byte is 0 + - no externally specified encoding + - no encoding declaration + by assuming UTF-16LE. But we don't, because this would mean when + presented just with a single byte, we couldn't reliably determine + whether we needed further bytes. + */ + if (state == XML_CONTENT_STATE) + break; + *encPtr = encodingTable[UTF_16LE_ENC]; + return XmlTok(*encPtr, state, ptr, end, nextTokPtr); + } + break; + } + } + *encPtr = encodingTable[INIT_ENC_INDEX(enc)]; + return XmlTok(*encPtr, state, ptr, end, nextTokPtr); +} + +#define NS(x) x +#define ns(x) x +#define XML_TOK_NS_C +#include "xmltok_ns.c" +#undef XML_TOK_NS_C +#undef NS +#undef ns + +#ifdef XML_NS + +# define NS(x) x##NS +# define ns(x) x##_ns + +# define XML_TOK_NS_C +# include "xmltok_ns.c" +# undef XML_TOK_NS_C + +# undef NS +# undef ns + +ENCODING * +XmlInitUnknownEncodingNS(void *mem, const int *table, CONVERTER convert, + void *userData) { + ENCODING *enc = XmlInitUnknownEncoding(mem, table, convert, userData); + if (enc) + ((struct normal_encoding *)enc)->type[ASCII_COLON] = BT_COLON; + return enc; +} + +#endif /* XML_NS */ diff --git a/port/third_party/expat/xmltok.h b/port/third_party/expat/xmltok.h new file mode 100644 index 00000000..bf641af6 --- /dev/null +++ b/port/third_party/expat/xmltok.h @@ -0,0 +1,317 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2002-2005 Karl Waclawek + Copyright (c) 2016-2026 Sebastian Pipping + Copyright (c) 2017 Rhodri James + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#ifndef XmlTok_INCLUDED +# define XmlTok_INCLUDED 1 + +# include // uint64_t + +/* The following token may be returned by XmlContentTok */ +# define XML_TOK_TRAILING_RSQB \ + -5 /* ] or ]] at the end of the scan; might be \ + start of illegal ]]> sequence */ +/* The following tokens may be returned by both XmlPrologTok and + XmlContentTok. +*/ +# define XML_TOK_NONE -4 /* The string to be scanned is empty */ +# define XML_TOK_TRAILING_CR \ + -3 /* A CR at the end of the scan; \ + might be part of CRLF sequence */ +# define XML_TOK_PARTIAL_CHAR -2 /* only part of a multibyte sequence */ +# define XML_TOK_PARTIAL -1 /* only part of a token */ +# define XML_TOK_INVALID 0 + +/* The following tokens are returned by XmlContentTok; some are also + returned by XmlAttributeValueTok, XmlEntityTok, XmlCdataSectionTok. +*/ +# define XML_TOK_START_TAG_WITH_ATTS 1 +# define XML_TOK_START_TAG_NO_ATTS 2 +# define XML_TOK_EMPTY_ELEMENT_WITH_ATTS 3 /* empty element tag */ +# define XML_TOK_EMPTY_ELEMENT_NO_ATTS 4 +# define XML_TOK_END_TAG 5 +# define XML_TOK_DATA_CHARS 6 +# define XML_TOK_DATA_NEWLINE 7 +# define XML_TOK_CDATA_SECT_OPEN 8 +# define XML_TOK_ENTITY_REF 9 +# define XML_TOK_CHAR_REF 10 /* numeric character reference */ + +/* The following tokens may be returned by both XmlPrologTok and + XmlContentTok. +*/ +# define XML_TOK_PI 11 /* processing instruction */ +# define XML_TOK_XML_DECL 12 /* XML decl or text decl */ +# define XML_TOK_COMMENT 13 +# define XML_TOK_BOM 14 /* Byte order mark */ + +/* The following tokens are returned only by XmlPrologTok */ +# define XML_TOK_PROLOG_S 15 +# define XML_TOK_DECL_OPEN 16 /* */ +# define XML_TOK_NAME 18 +# define XML_TOK_NMTOKEN 19 +# define XML_TOK_POUND_NAME 20 /* #name */ +# define XML_TOK_OR 21 /* | */ +# define XML_TOK_PERCENT 22 +# define XML_TOK_OPEN_PAREN 23 +# define XML_TOK_CLOSE_PAREN 24 +# define XML_TOK_OPEN_BRACKET 25 +# define XML_TOK_CLOSE_BRACKET 26 +# define XML_TOK_LITERAL 27 +# define XML_TOK_PARAM_ENTITY_REF 28 +# define XML_TOK_INSTANCE_START 29 + +/* The following occur only in element type declarations */ +# define XML_TOK_NAME_QUESTION 30 /* name? */ +# define XML_TOK_NAME_ASTERISK 31 /* name* */ +# define XML_TOK_NAME_PLUS 32 /* name+ */ +# define XML_TOK_COND_SECT_OPEN 33 /* */ +# define XML_TOK_CLOSE_PAREN_QUESTION 35 /* )? */ +# define XML_TOK_CLOSE_PAREN_ASTERISK 36 /* )* */ +# define XML_TOK_CLOSE_PAREN_PLUS 37 /* )+ */ +# define XML_TOK_COMMA 38 + +/* The following token is returned only by XmlAttributeValueTok */ +# define XML_TOK_ATTRIBUTE_VALUE_S 39 + +/* The following token is returned only by XmlCdataSectionTok */ +# define XML_TOK_CDATA_SECT_CLOSE 40 + +/* With namespace processing this is returned by XmlPrologTok for a + name with a colon. +*/ +# define XML_TOK_PREFIXED_NAME 41 + +# ifdef XML_DTD +# define XML_TOK_IGNORE_SECT 42 +# endif /* XML_DTD */ + +# ifdef XML_DTD +# define XML_N_STATES 4 +# else /* not XML_DTD */ +# define XML_N_STATES 3 +# endif /* not XML_DTD */ + +# define XML_PROLOG_STATE 0 +# define XML_CONTENT_STATE 1 +# define XML_CDATA_SECTION_STATE 2 +# ifdef XML_DTD +# define XML_IGNORE_SECTION_STATE 3 +# endif /* XML_DTD */ + +# define XML_N_LITERAL_TYPES 2 +# define XML_ATTRIBUTE_VALUE_LITERAL 0 +# define XML_ENTITY_VALUE_LITERAL 1 + +/* The size of the buffer passed to XmlUtf8Encode must be at least this. */ +# define XML_UTF8_ENCODE_MAX 4 +/* The size of the buffer passed to XmlUtf16Encode must be at least this. */ +# define XML_UTF16_ENCODE_MAX 2 + +typedef struct position { + /* first line and first column are 0 not 1 */ + uint64_t lineNumber; + uint64_t columnNumber; +} POSITION; + +typedef struct { + const char *name; + const char *valuePtr; + const char *valueEnd; + char normalized; +} ATTRIBUTE; + +struct encoding; +typedef struct encoding ENCODING; + +typedef int (*SCANNER)(const ENCODING *, const char *, const char *, + const char **); + +enum XML_Convert_Result { + XML_CONVERT_COMPLETED = 0, + XML_CONVERT_INPUT_INCOMPLETE = 1, + XML_CONVERT_OUTPUT_EXHAUSTED = 2 /* and therefore potentially input remaining + as well */ +}; + +struct encoding { + SCANNER scanners[XML_N_STATES]; + SCANNER literalScanners[XML_N_LITERAL_TYPES]; + int (*nameMatchesAscii)(const ENCODING *, const char *, const char *, + const char *); + int (*nameLength)(const ENCODING *, const char *); + const char *(*skipS)(const ENCODING *, const char *); + int (*getAtts)(const ENCODING *enc, const char *ptr, int attsMax, + ATTRIBUTE *atts); + int (*charRefNumber)(const ENCODING *enc, const char *ptr); + int (*predefinedEntityName)(const ENCODING *, const char *, const char *); + void (*updatePosition)(const ENCODING *, const char *ptr, const char *end, + POSITION *); + int (*isPublicId)(const ENCODING *enc, const char *ptr, const char *end, + const char **badPtr); + enum XML_Convert_Result (*utf8Convert)(const ENCODING *enc, + const char **fromP, + const char *fromLim, char **toP, + const char *toLim); + enum XML_Convert_Result (*utf16Convert)(const ENCODING *enc, + const char **fromP, + const char *fromLim, + unsigned short **toP, + const unsigned short *toLim); + int minBytesPerChar; + char isUtf8; + char isUtf16; +}; + +/* Scan the string starting at ptr until the end of the next complete + token, but do not scan past eptr. Return an integer giving the + type of token. + + Return XML_TOK_NONE when ptr == eptr; nextTokPtr will not be set. + + Return XML_TOK_PARTIAL when the string does not contain a complete + token; nextTokPtr will not be set. + + Return XML_TOK_INVALID when the string does not start a valid + token; nextTokPtr will be set to point to the character which made + the token invalid. + + Otherwise the string starts with a valid token; nextTokPtr will be + set to point to the character following the end of that token. + + Each data character counts as a single token, but adjacent data + characters may be returned together. Similarly for characters in + the prolog outside literals, comments and processing instructions. +*/ + +# define XmlTok(enc, state, ptr, end, nextTokPtr) \ + (((enc)->scanners[state])(enc, ptr, end, nextTokPtr)) + +# define XmlPrologTok(enc, ptr, end, nextTokPtr) \ + XmlTok(enc, XML_PROLOG_STATE, ptr, end, nextTokPtr) + +# define XmlContentTok(enc, ptr, end, nextTokPtr) \ + XmlTok(enc, XML_CONTENT_STATE, ptr, end, nextTokPtr) + +# define XmlCdataSectionTok(enc, ptr, end, nextTokPtr) \ + XmlTok(enc, XML_CDATA_SECTION_STATE, ptr, end, nextTokPtr) + +# ifdef XML_DTD + +# define XmlIgnoreSectionTok(enc, ptr, end, nextTokPtr) \ + XmlTok(enc, XML_IGNORE_SECTION_STATE, ptr, end, nextTokPtr) + +# endif /* XML_DTD */ + +/* This is used for performing a 2nd-level tokenization on the content + of a literal that has already been returned by XmlTok. +*/ +# define XmlLiteralTok(enc, literalType, ptr, end, nextTokPtr) \ + (((enc)->literalScanners[literalType])(enc, ptr, end, nextTokPtr)) + +# define XmlAttributeValueTok(enc, ptr, end, nextTokPtr) \ + XmlLiteralTok(enc, XML_ATTRIBUTE_VALUE_LITERAL, ptr, end, nextTokPtr) + +# define XmlEntityValueTok(enc, ptr, end, nextTokPtr) \ + XmlLiteralTok(enc, XML_ENTITY_VALUE_LITERAL, ptr, end, nextTokPtr) + +# define XmlNameMatchesAscii(enc, ptr1, end1, ptr2) \ + (((enc)->nameMatchesAscii)(enc, ptr1, end1, ptr2)) + +# define XmlNameLength(enc, ptr) (((enc)->nameLength)(enc, ptr)) + +# define XmlSkipS(enc, ptr) (((enc)->skipS)(enc, ptr)) + +# define XmlGetAttributes(enc, ptr, attsMax, atts) \ + (((enc)->getAtts)(enc, ptr, attsMax, atts)) + +# define XmlCharRefNumber(enc, ptr) (((enc)->charRefNumber)(enc, ptr)) + +# define XmlPredefinedEntityName(enc, ptr, end) \ + (((enc)->predefinedEntityName)(enc, ptr, end)) + +# define XmlUpdatePosition(enc, ptr, end, pos) \ + (((enc)->updatePosition)(enc, ptr, end, pos)) + +# define XmlIsPublicId(enc, ptr, end, badPtr) \ + (((enc)->isPublicId)(enc, ptr, end, badPtr)) + +# define XmlUtf8Convert(enc, fromP, fromLim, toP, toLim) \ + (((enc)->utf8Convert)(enc, fromP, fromLim, toP, toLim)) + +# define XmlUtf16Convert(enc, fromP, fromLim, toP, toLim) \ + (((enc)->utf16Convert)(enc, fromP, fromLim, toP, toLim)) + +typedef struct { + ENCODING initEnc; + const ENCODING **encPtr; +} INIT_ENCODING; + +int XmlParseXmlDecl(int isGeneralTextEntity, const ENCODING *enc, + const char *ptr, const char *end, const char **badPtr, + const char **versionPtr, const char **versionEndPtr, + const char **encodingNamePtr, + const ENCODING **namedEncodingPtr, int *standalonePtr); + +int XmlInitEncoding(INIT_ENCODING *p, const ENCODING **encPtr, + const char *name); +const ENCODING *XmlGetUtf8InternalEncoding(void); +const ENCODING *XmlGetUtf16InternalEncoding(void); +int XmlUtf8Encode(int charNumber, char *buf); +int XmlUtf16Encode(int charNumber, unsigned short *buf); +int XmlSizeOfUnknownEncoding(void); + +typedef int(XMLCALL *CONVERTER)(void *userData, const char *p); + +ENCODING *XmlInitUnknownEncoding(void *mem, const int *table, CONVERTER convert, + void *userData); + +int XmlParseXmlDeclNS(int isGeneralTextEntity, const ENCODING *enc, + const char *ptr, const char *end, const char **badPtr, + const char **versionPtr, const char **versionEndPtr, + const char **encodingNamePtr, + const ENCODING **namedEncodingPtr, int *standalonePtr); + +int XmlInitEncodingNS(INIT_ENCODING *p, const ENCODING **encPtr, + const char *name); +const ENCODING *XmlGetUtf8InternalEncodingNS(void); +const ENCODING *XmlGetUtf16InternalEncodingNS(void); +ENCODING *XmlInitUnknownEncodingNS(void *mem, const int *table, + CONVERTER convert, void *userData); + +#endif /* not XmlTok_INCLUDED */ diff --git a/port/third_party/expat/xmltok_impl.c b/port/third_party/expat/xmltok_impl.c new file mode 100644 index 00000000..a71a2fd8 --- /dev/null +++ b/port/third_party/expat/xmltok_impl.c @@ -0,0 +1,1819 @@ +/* This file is included (from xmltok.c, 1-3 times depending on XML_MIN_SIZE)! + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2002-2016 Karl Waclawek + Copyright (c) 2016-2026 Sebastian Pipping + Copyright (c) 2017 Rhodri James + Copyright (c) 2018 Benjamin Peterson + Copyright (c) 2018 Anton Maklakov + Copyright (c) 2019 David Loffredo + Copyright (c) 2020 Boris Kolpackov + Copyright (c) 2022 Martin Ettl + Copyright (c) 2026 Nick Begg + Copyright (c) 2026 Kartik Kenchi + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#ifdef XML_TOK_IMPL_C + +# define INVALID_LEAD_CASE(n, ptr, nextTokPtr) \ + case BT_LEAD##n: \ + if (end - ptr < n) \ + return XML_TOK_PARTIAL_CHAR; \ + if (IS_INVALID_CHAR(enc, ptr, n)) { \ + *(nextTokPtr) = (ptr); \ + return XML_TOK_INVALID; \ + } \ + ptr += n; \ + break; + +# define INVALID_CASES(ptr, nextTokPtr) \ + INVALID_LEAD_CASE(2, ptr, nextTokPtr) \ + INVALID_LEAD_CASE(3, ptr, nextTokPtr) \ + INVALID_LEAD_CASE(4, ptr, nextTokPtr) \ + case BT_NONXML: \ + case BT_MALFORM: \ + case BT_TRAIL: \ + *(nextTokPtr) = (ptr); \ + return XML_TOK_INVALID; + +# define CHECK_NAME_CASE(n, enc, ptr, end, nextTokPtr) \ + case BT_LEAD##n: \ + if (end - ptr < n) \ + return XML_TOK_PARTIAL_CHAR; \ + if (IS_INVALID_CHAR(enc, ptr, n) || ! IS_NAME_CHAR(enc, ptr, n)) { \ + *nextTokPtr = ptr; \ + return XML_TOK_INVALID; \ + } \ + ptr += n; \ + break; + +# define CHECK_NAME_CASES(enc, ptr, end, nextTokPtr) \ + case BT_NONASCII: \ + if (! IS_NAME_CHAR_MINBPC(enc, ptr)) { \ + *nextTokPtr = ptr; \ + return XML_TOK_INVALID; \ + } \ + EXPAT_FALLTHROUGH; \ + case BT_NMSTRT: \ + case BT_HEX: \ + case BT_DIGIT: \ + case BT_NAME: \ + case BT_MINUS: \ + ptr += MINBPC(enc); \ + break; \ + CHECK_NAME_CASE(2, enc, ptr, end, nextTokPtr) \ + CHECK_NAME_CASE(3, enc, ptr, end, nextTokPtr) \ + CHECK_NAME_CASE(4, enc, ptr, end, nextTokPtr) + +# define CHECK_NMSTRT_CASE(n, enc, ptr, end, nextTokPtr) \ + case BT_LEAD##n: \ + if ((end) - (ptr) < (n)) \ + return XML_TOK_PARTIAL_CHAR; \ + if (IS_INVALID_CHAR(enc, ptr, n) || ! IS_NMSTRT_CHAR(enc, ptr, n)) { \ + *nextTokPtr = ptr; \ + return XML_TOK_INVALID; \ + } \ + ptr += n; \ + break; + +# define CHECK_NMSTRT_CASES(enc, ptr, end, nextTokPtr) \ + case BT_NONASCII: \ + if (! IS_NMSTRT_CHAR_MINBPC(enc, ptr)) { \ + *nextTokPtr = ptr; \ + return XML_TOK_INVALID; \ + } \ + EXPAT_FALLTHROUGH; \ + case BT_NMSTRT: \ + case BT_HEX: \ + ptr += MINBPC(enc); \ + break; \ + CHECK_NMSTRT_CASE(2, enc, ptr, end, nextTokPtr) \ + CHECK_NMSTRT_CASE(3, enc, ptr, end, nextTokPtr) \ + CHECK_NMSTRT_CASE(4, enc, ptr, end, nextTokPtr) + +# ifndef PREFIX +# define PREFIX(ident) ident +# endif + +# define HAS_CHARS(enc, ptr, end, count) \ + ((end) - (ptr) >= ((count) * MINBPC(enc))) + +# define HAS_CHAR(enc, ptr, end) HAS_CHARS(enc, ptr, end, 1) + +# define REQUIRE_CHARS(enc, ptr, end, count) \ + { \ + if (! HAS_CHARS(enc, ptr, end, count)) { \ + return XML_TOK_PARTIAL; \ + } \ + } + +# define REQUIRE_CHAR(enc, ptr, end) REQUIRE_CHARS(enc, ptr, end, 1) + +/* ptr points to character following " */ + switch (BYTE_TYPE(enc, ptr + MINBPC(enc))) { + case BT_S: + case BT_CR: + case BT_LF: + case BT_PERCNT: + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + EXPAT_FALLTHROUGH; + case BT_S: + case BT_CR: + case BT_LF: + *nextTokPtr = ptr; + return XML_TOK_DECL_OPEN; + case BT_NMSTRT: + case BT_HEX: + ptr += MINBPC(enc); + break; + default: + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + } + return XML_TOK_PARTIAL; +} + +static int +PREFIX(checkPiTarget)(const ENCODING *enc, const char *ptr, const char *end, + int *tokPtr) { + int upper = 0; + UNUSED_P(enc); + *tokPtr = XML_TOK_PI; + if (end - ptr != MINBPC(enc) * 3) + return 1; + switch (BYTE_TO_ASCII(enc, ptr)) { + case ASCII_x: + break; + case ASCII_X: + upper = 1; + break; + default: + return 1; + } + ptr += MINBPC(enc); + switch (BYTE_TO_ASCII(enc, ptr)) { + case ASCII_m: + break; + case ASCII_M: + upper = 1; + break; + default: + return 1; + } + ptr += MINBPC(enc); + switch (BYTE_TO_ASCII(enc, ptr)) { + case ASCII_l: + break; + case ASCII_L: + upper = 1; + break; + default: + return 1; + } + if (upper) + return 0; + *tokPtr = XML_TOK_XML_DECL; + return 1; +} + +/* ptr points to character following "= end) + return XML_TOK_NONE; + if (MINBPC(enc) > 1) { + size_t n = end - ptr; + if (n & (MINBPC(enc) - 1)) { + n &= ~(MINBPC(enc) - 1); + if (n == 0) + return XML_TOK_PARTIAL; + end = ptr + n; + } + } + switch (BYTE_TYPE(enc, ptr)) { + case BT_RSQB: + ptr += MINBPC(enc); + REQUIRE_CHAR(enc, ptr, end); + if (! CHAR_MATCHES(enc, ptr, ASCII_RSQB)) + break; + ptr += MINBPC(enc); + REQUIRE_CHAR(enc, ptr, end); + if (! CHAR_MATCHES(enc, ptr, ASCII_GT)) { + ptr -= MINBPC(enc); + break; + } + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_CDATA_SECT_CLOSE; + case BT_CR: + ptr += MINBPC(enc); + REQUIRE_CHAR(enc, ptr, end); + if (BYTE_TYPE(enc, ptr) == BT_LF) + ptr += MINBPC(enc); + *nextTokPtr = ptr; + return XML_TOK_DATA_NEWLINE; + case BT_LF: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_DATA_NEWLINE; + INVALID_CASES(ptr, nextTokPtr) + default: + ptr += MINBPC(enc); + break; + } + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { +# define LEAD_CASE(n) \ + case BT_LEAD##n: \ + if (end - ptr < n || IS_INVALID_CHAR(enc, ptr, n)) { \ + *nextTokPtr = ptr; \ + return XML_TOK_DATA_CHARS; \ + } \ + ptr += n; \ + break; + LEAD_CASE(2) + LEAD_CASE(3) + LEAD_CASE(4) +# undef LEAD_CASE + case BT_NONXML: + case BT_MALFORM: + case BT_TRAIL: + case BT_CR: + case BT_LF: + case BT_RSQB: + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + default: + ptr += MINBPC(enc); + break; + } + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; +} + +/* ptr points to character following "= end) + return XML_TOK_NONE; + if (MINBPC(enc) > 1) { + size_t n = end - ptr; + if (n & (MINBPC(enc) - 1)) { + n &= ~(MINBPC(enc) - 1); + if (n == 0) + return XML_TOK_PARTIAL; + end = ptr + n; + } + } + switch (BYTE_TYPE(enc, ptr)) { + case BT_LT: + return PREFIX(scanLt)(enc, ptr + MINBPC(enc), end, nextTokPtr); + case BT_AMP: + return PREFIX(scanRef)(enc, ptr + MINBPC(enc), end, nextTokPtr); + case BT_CR: + ptr += MINBPC(enc); + if (! HAS_CHAR(enc, ptr, end)) + return XML_TOK_TRAILING_CR; + if (BYTE_TYPE(enc, ptr) == BT_LF) + ptr += MINBPC(enc); + *nextTokPtr = ptr; + return XML_TOK_DATA_NEWLINE; + case BT_LF: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_DATA_NEWLINE; + case BT_RSQB: + ptr += MINBPC(enc); + if (! HAS_CHAR(enc, ptr, end)) + return XML_TOK_TRAILING_RSQB; + if (! CHAR_MATCHES(enc, ptr, ASCII_RSQB)) + break; + ptr += MINBPC(enc); + if (! HAS_CHAR(enc, ptr, end)) + return XML_TOK_TRAILING_RSQB; + if (! CHAR_MATCHES(enc, ptr, ASCII_GT)) { + ptr -= MINBPC(enc); + break; + } + *nextTokPtr = ptr; + return XML_TOK_INVALID; + INVALID_CASES(ptr, nextTokPtr) + default: + ptr += MINBPC(enc); + break; + } + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { +# define LEAD_CASE(n) \ + case BT_LEAD##n: \ + if (end - ptr < n || IS_INVALID_CHAR(enc, ptr, n)) { \ + *nextTokPtr = ptr; \ + return XML_TOK_DATA_CHARS; \ + } \ + ptr += n; \ + break; + LEAD_CASE(2) + LEAD_CASE(3) + LEAD_CASE(4) +# undef LEAD_CASE + case BT_RSQB: + if (HAS_CHARS(enc, ptr, end, 2)) { + if (! CHAR_MATCHES(enc, ptr + MINBPC(enc), ASCII_RSQB)) { + ptr += MINBPC(enc); + break; + } + if (HAS_CHARS(enc, ptr, end, 3)) { + if (! CHAR_MATCHES(enc, ptr + 2 * MINBPC(enc), ASCII_GT)) { + ptr += MINBPC(enc); + break; + } + *nextTokPtr = ptr + 2 * MINBPC(enc); + return XML_TOK_INVALID; + } + } + EXPAT_FALLTHROUGH; + case BT_AMP: + case BT_LT: + case BT_NONXML: + case BT_MALFORM: + case BT_TRAIL: + case BT_CR: + case BT_LF: + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + default: + ptr += MINBPC(enc); + break; + } + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; +} + +/* ptr points to character following "%" */ + +static int +PREFIX(scanPercent)(const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + REQUIRE_CHAR(enc, ptr, end); + switch (BYTE_TYPE(enc, ptr)) { + CHECK_NMSTRT_CASES(enc, ptr, end, nextTokPtr) + case BT_S: + case BT_LF: + case BT_CR: + case BT_PERCNT: + *nextTokPtr = ptr; + return XML_TOK_PERCENT; + default: + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { + CHECK_NAME_CASES(enc, ptr, end, nextTokPtr) + case BT_SEMI: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_PARAM_ENTITY_REF; + default: + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + } + return XML_TOK_PARTIAL; +} + +static int +PREFIX(scanPoundName)(const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + REQUIRE_CHAR(enc, ptr, end); + switch (BYTE_TYPE(enc, ptr)) { + CHECK_NMSTRT_CASES(enc, ptr, end, nextTokPtr) + default: + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { + CHECK_NAME_CASES(enc, ptr, end, nextTokPtr) + case BT_CR: + case BT_LF: + case BT_S: + case BT_RPAR: + case BT_GT: + case BT_PERCNT: + case BT_VERBAR: + *nextTokPtr = ptr; + return XML_TOK_POUND_NAME; + default: + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + } + return -XML_TOK_POUND_NAME; +} + +static int +PREFIX(scanLit)(int open, const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + while (HAS_CHAR(enc, ptr, end)) { + int t = BYTE_TYPE(enc, ptr); + switch (t) { + INVALID_CASES(ptr, nextTokPtr) + case BT_QUOT: + case BT_APOS: + ptr += MINBPC(enc); + if (t != open) + break; + if (! HAS_CHAR(enc, ptr, end)) + return -XML_TOK_LITERAL; + *nextTokPtr = ptr; + switch (BYTE_TYPE(enc, ptr)) { + case BT_S: + case BT_CR: + case BT_LF: + case BT_GT: + case BT_PERCNT: + case BT_LSQB: + return XML_TOK_LITERAL; + default: + return XML_TOK_INVALID; + } + default: + ptr += MINBPC(enc); + break; + } + } + return XML_TOK_PARTIAL; +} + +static int +PREFIX(prologTok)(const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + int tok; + if (ptr >= end) + return XML_TOK_NONE; + if (MINBPC(enc) > 1) { + size_t n = end - ptr; + if (n & (MINBPC(enc) - 1)) { + n &= ~(MINBPC(enc) - 1); + if (n == 0) + return XML_TOK_PARTIAL; + end = ptr + n; + } + } + switch (BYTE_TYPE(enc, ptr)) { + case BT_QUOT: + return PREFIX(scanLit)(BT_QUOT, enc, ptr + MINBPC(enc), end, nextTokPtr); + case BT_APOS: + return PREFIX(scanLit)(BT_APOS, enc, ptr + MINBPC(enc), end, nextTokPtr); + case BT_LT: { + ptr += MINBPC(enc); + REQUIRE_CHAR(enc, ptr, end); + switch (BYTE_TYPE(enc, ptr)) { + case BT_EXCL: + return PREFIX(scanDecl)(enc, ptr + MINBPC(enc), end, nextTokPtr); + case BT_QUEST: + return PREFIX(scanPi)(enc, ptr + MINBPC(enc), end, nextTokPtr); + case BT_NMSTRT: + case BT_HEX: + case BT_NONASCII: + case BT_LEAD2: + case BT_LEAD3: + case BT_LEAD4: + *nextTokPtr = ptr - MINBPC(enc); + return XML_TOK_INSTANCE_START; + } + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + case BT_CR: + if (ptr + MINBPC(enc) == end) { + *nextTokPtr = end; + /* indicate that this might be part of a CR/LF pair */ + return -XML_TOK_PROLOG_S; + } + EXPAT_FALLTHROUGH; + case BT_S: + case BT_LF: + for (;;) { + ptr += MINBPC(enc); + if (! HAS_CHAR(enc, ptr, end)) + break; + switch (BYTE_TYPE(enc, ptr)) { + case BT_S: + case BT_LF: + break; + case BT_CR: + /* don't split CR/LF pair */ + if (ptr + MINBPC(enc) != end) + break; + EXPAT_FALLTHROUGH; + default: + *nextTokPtr = ptr; + return XML_TOK_PROLOG_S; + } + } + *nextTokPtr = ptr; + return XML_TOK_PROLOG_S; + case BT_PERCNT: + return PREFIX(scanPercent)(enc, ptr + MINBPC(enc), end, nextTokPtr); + case BT_COMMA: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_COMMA; + case BT_LSQB: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_OPEN_BRACKET; + case BT_RSQB: + ptr += MINBPC(enc); + if (! HAS_CHAR(enc, ptr, end)) + return -XML_TOK_CLOSE_BRACKET; + if (CHAR_MATCHES(enc, ptr, ASCII_RSQB)) { + REQUIRE_CHARS(enc, ptr, end, 2); + if (CHAR_MATCHES(enc, ptr + MINBPC(enc), ASCII_GT)) { + *nextTokPtr = ptr + 2 * MINBPC(enc); + return XML_TOK_COND_SECT_CLOSE; + } + } + *nextTokPtr = ptr; + return XML_TOK_CLOSE_BRACKET; + case BT_LPAR: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_OPEN_PAREN; + case BT_RPAR: + ptr += MINBPC(enc); + if (! HAS_CHAR(enc, ptr, end)) + return -XML_TOK_CLOSE_PAREN; + switch (BYTE_TYPE(enc, ptr)) { + case BT_AST: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_CLOSE_PAREN_ASTERISK; + case BT_QUEST: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_CLOSE_PAREN_QUESTION; + case BT_PLUS: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_CLOSE_PAREN_PLUS; + case BT_CR: + case BT_LF: + case BT_S: + case BT_GT: + case BT_COMMA: + case BT_VERBAR: + case BT_RPAR: + *nextTokPtr = ptr; + return XML_TOK_CLOSE_PAREN; + } + *nextTokPtr = ptr; + return XML_TOK_INVALID; + case BT_VERBAR: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_OR; + case BT_GT: + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_DECL_CLOSE; + case BT_NUM: + return PREFIX(scanPoundName)(enc, ptr + MINBPC(enc), end, nextTokPtr); +# define LEAD_CASE(n) \ + case BT_LEAD##n: \ + if (end - ptr < n) \ + return XML_TOK_PARTIAL_CHAR; \ + if (IS_INVALID_CHAR(enc, ptr, n)) { \ + *nextTokPtr = ptr; \ + return XML_TOK_INVALID; \ + } \ + if (IS_NMSTRT_CHAR(enc, ptr, n)) { \ + ptr += n; \ + tok = XML_TOK_NAME; \ + break; \ + } \ + if (IS_NAME_CHAR(enc, ptr, n)) { \ + ptr += n; \ + tok = XML_TOK_NMTOKEN; \ + break; \ + } \ + *nextTokPtr = ptr; \ + return XML_TOK_INVALID; + LEAD_CASE(2) + LEAD_CASE(3) + LEAD_CASE(4) +# undef LEAD_CASE + case BT_NMSTRT: + case BT_HEX: + tok = XML_TOK_NAME; + ptr += MINBPC(enc); + break; + case BT_DIGIT: + case BT_NAME: + case BT_MINUS: +# ifdef XML_NS + case BT_COLON: +# endif + tok = XML_TOK_NMTOKEN; + ptr += MINBPC(enc); + break; + case BT_NONASCII: + if (IS_NMSTRT_CHAR_MINBPC(enc, ptr)) { + ptr += MINBPC(enc); + tok = XML_TOK_NAME; + break; + } + if (IS_NAME_CHAR_MINBPC(enc, ptr)) { + ptr += MINBPC(enc); + tok = XML_TOK_NMTOKEN; + break; + } + EXPAT_FALLTHROUGH; + default: + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { + CHECK_NAME_CASES(enc, ptr, end, nextTokPtr) + case BT_GT: + case BT_RPAR: + case BT_COMMA: + case BT_VERBAR: + case BT_LSQB: + case BT_PERCNT: + case BT_S: + case BT_CR: + case BT_LF: + *nextTokPtr = ptr; + return tok; +# ifdef XML_NS + case BT_COLON: + ptr += MINBPC(enc); + switch (tok) { + case XML_TOK_NAME: + REQUIRE_CHAR(enc, ptr, end); + tok = XML_TOK_PREFIXED_NAME; + switch (BYTE_TYPE(enc, ptr)) { + CHECK_NAME_CASES(enc, ptr, end, nextTokPtr) + default: + tok = XML_TOK_NMTOKEN; + break; + } + break; + case XML_TOK_PREFIXED_NAME: + tok = XML_TOK_NMTOKEN; + break; + } + break; +# endif + case BT_PLUS: + if (tok == XML_TOK_NMTOKEN) { + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_NAME_PLUS; + case BT_AST: + if (tok == XML_TOK_NMTOKEN) { + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_NAME_ASTERISK; + case BT_QUEST: + if (tok == XML_TOK_NMTOKEN) { + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_NAME_QUESTION; + default: + *nextTokPtr = ptr; + return XML_TOK_INVALID; + } + } + return -tok; +} + +static int +PREFIX(attributeValueTok)(const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + const char *start; + if (ptr >= end) + return XML_TOK_NONE; + else if (! HAS_CHAR(enc, ptr, end)) { + /* This line cannot be executed. The incoming data has already + * been tokenized once, so incomplete characters like this have + * already been eliminated from the input. Retaining the paranoia + * check is still valuable, however. + */ + return XML_TOK_PARTIAL; /* LCOV_EXCL_LINE */ + } + start = ptr; + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { +# define LEAD_CASE(n) \ + case BT_LEAD##n: \ + ptr += n; /* NOTE: The encoding has already been validated. */ \ + break; + LEAD_CASE(2) + LEAD_CASE(3) + LEAD_CASE(4) +# undef LEAD_CASE + case BT_AMP: + if (ptr == start) + return PREFIX(scanRef)(enc, ptr + MINBPC(enc), end, nextTokPtr); + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + case BT_LT: + /* this is for inside entity references */ + *nextTokPtr = ptr; + return XML_TOK_INVALID; + case BT_LF: + if (ptr == start) { + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_DATA_NEWLINE; + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + case BT_CR: + if (ptr == start) { + ptr += MINBPC(enc); + if (! HAS_CHAR(enc, ptr, end)) + return XML_TOK_TRAILING_CR; + if (BYTE_TYPE(enc, ptr) == BT_LF) + ptr += MINBPC(enc); + *nextTokPtr = ptr; + return XML_TOK_DATA_NEWLINE; + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + case BT_S: + if (ptr == start) { + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_ATTRIBUTE_VALUE_S; + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + default: + ptr += MINBPC(enc); + break; + } + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; +} + +static int +PREFIX(entityValueTok)(const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + const char *start; + if (ptr >= end) + return XML_TOK_NONE; + else if (! HAS_CHAR(enc, ptr, end)) { + /* This line cannot be executed. The incoming data has already + * been tokenized once, so incomplete characters like this have + * already been eliminated from the input. Retaining the paranoia + * check is still valuable, however. + */ + return XML_TOK_PARTIAL; /* LCOV_EXCL_LINE */ + } + start = ptr; + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { +# define LEAD_CASE(n) \ + case BT_LEAD##n: \ + ptr += n; /* NOTE: The encoding has already been validated. */ \ + break; + LEAD_CASE(2) + LEAD_CASE(3) + LEAD_CASE(4) +# undef LEAD_CASE + case BT_AMP: + if (ptr == start) + return PREFIX(scanRef)(enc, ptr + MINBPC(enc), end, nextTokPtr); + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + case BT_PERCNT: + if (ptr == start) { + int tok = PREFIX(scanPercent)(enc, ptr + MINBPC(enc), end, nextTokPtr); + return (tok == XML_TOK_PERCENT) ? XML_TOK_INVALID : tok; + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + case BT_LF: + if (ptr == start) { + *nextTokPtr = ptr + MINBPC(enc); + return XML_TOK_DATA_NEWLINE; + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + case BT_CR: + if (ptr == start) { + ptr += MINBPC(enc); + if (! HAS_CHAR(enc, ptr, end)) + return XML_TOK_TRAILING_CR; + if (BYTE_TYPE(enc, ptr) == BT_LF) + ptr += MINBPC(enc); + *nextTokPtr = ptr; + return XML_TOK_DATA_NEWLINE; + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; + default: + ptr += MINBPC(enc); + break; + } + } + *nextTokPtr = ptr; + return XML_TOK_DATA_CHARS; +} + +# ifdef XML_DTD + +static int +PREFIX(ignoreSectionTok)(const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + int level = 0; + if (MINBPC(enc) > 1) { + size_t n = end - ptr; + if (n & (MINBPC(enc) - 1)) { + n &= ~(MINBPC(enc) - 1); + end = ptr + n; + } + } + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { + INVALID_CASES(ptr, nextTokPtr) + case BT_LT: + ptr += MINBPC(enc); + REQUIRE_CHAR(enc, ptr, end); + if (CHAR_MATCHES(enc, ptr, ASCII_EXCL)) { + ptr += MINBPC(enc); + REQUIRE_CHAR(enc, ptr, end); + if (CHAR_MATCHES(enc, ptr, ASCII_LSQB)) { + ++level; + ptr += MINBPC(enc); + } + } + break; + case BT_RSQB: + ptr += MINBPC(enc); + REQUIRE_CHAR(enc, ptr, end); + if (CHAR_MATCHES(enc, ptr, ASCII_RSQB)) { + ptr += MINBPC(enc); + REQUIRE_CHAR(enc, ptr, end); + if (CHAR_MATCHES(enc, ptr, ASCII_GT)) { + ptr += MINBPC(enc); + if (level == 0) { + *nextTokPtr = ptr; + return XML_TOK_IGNORE_SECT; + } + --level; + } + } + break; + default: + ptr += MINBPC(enc); + break; + } + } + return XML_TOK_PARTIAL; +} + +# endif /* XML_DTD */ + +static int +PREFIX(isPublicId)(const ENCODING *enc, const char *ptr, const char *end, + const char **badPtr) { + ptr += MINBPC(enc); + end -= MINBPC(enc); + for (; HAS_CHAR(enc, ptr, end); ptr += MINBPC(enc)) { + switch (BYTE_TYPE(enc, ptr)) { + case BT_DIGIT: + case BT_HEX: + case BT_MINUS: + case BT_APOS: + case BT_LPAR: + case BT_RPAR: + case BT_PLUS: + case BT_COMMA: + case BT_SOL: + case BT_EQUALS: + case BT_QUEST: + case BT_CR: + case BT_LF: + case BT_SEMI: + case BT_EXCL: + case BT_AST: + case BT_PERCNT: + case BT_NUM: +# ifdef XML_NS + case BT_COLON: +# endif + break; + case BT_S: + if (CHAR_MATCHES(enc, ptr, ASCII_TAB)) { + *badPtr = ptr; + return 0; + } + break; + case BT_NAME: + case BT_NMSTRT: + if (! (BYTE_TO_ASCII(enc, ptr) & ~0x7f)) + break; + EXPAT_FALLTHROUGH; + default: + switch (BYTE_TO_ASCII(enc, ptr)) { + case 0x24: /* $ */ + case 0x40: /* @ */ + break; + default: + *badPtr = ptr; + return 0; + } + break; + } + } + return 1; +} + +/* This must only be called for a well-formed start-tag or empty + element tag. Returns the number of attributes. Pointers to the + first attsMax attributes are stored in atts. +*/ + +static int +PREFIX(getAtts)(const ENCODING *enc, const char *ptr, int attsMax, + ATTRIBUTE *atts) { + enum { other, inName, inValue } state = inName; + int nAtts = 0; + int open = 0; /* defined when state == inValue; + initialization just to shut up compilers */ + + for (ptr += MINBPC(enc);; ptr += MINBPC(enc)) { + switch (BYTE_TYPE(enc, ptr)) { +# define START_NAME \ + if (state == other) { \ + if (nAtts < attsMax) { \ + atts[nAtts].name = ptr; \ + atts[nAtts].normalized = 1; \ + } \ + state = inName; \ + } +# define LEAD_CASE(n) \ + case BT_LEAD##n: /* NOTE: The encoding has already been validated. */ \ + START_NAME ptr += (n - MINBPC(enc)); \ + break; + LEAD_CASE(2) + LEAD_CASE(3) + LEAD_CASE(4) +# undef LEAD_CASE + case BT_NONASCII: + case BT_NMSTRT: + case BT_HEX: + START_NAME + break; +# undef START_NAME + case BT_QUOT: + if (state != inValue) { + if (nAtts < attsMax) + atts[nAtts].valuePtr = ptr + MINBPC(enc); + state = inValue; + open = BT_QUOT; + } else if (open == BT_QUOT) { + state = other; + if (nAtts < attsMax) + atts[nAtts].valueEnd = ptr; + nAtts++; + } + break; + case BT_APOS: + if (state != inValue) { + if (nAtts < attsMax) + atts[nAtts].valuePtr = ptr + MINBPC(enc); + state = inValue; + open = BT_APOS; + } else if (open == BT_APOS) { + state = other; + if (nAtts < attsMax) + atts[nAtts].valueEnd = ptr; + nAtts++; + } + break; + case BT_AMP: + if (nAtts < attsMax) + atts[nAtts].normalized = 0; + break; + case BT_S: + if (state == inName) + state = other; + else if (state == inValue && nAtts < attsMax && atts[nAtts].normalized + && (ptr == atts[nAtts].valuePtr + || BYTE_TO_ASCII(enc, ptr) != ASCII_SPACE + || BYTE_TO_ASCII(enc, ptr + MINBPC(enc)) == ASCII_SPACE + || BYTE_TYPE(enc, ptr + MINBPC(enc)) == open)) + atts[nAtts].normalized = 0; + break; + case BT_CR: + case BT_LF: + /* This case ensures that the first attribute name is counted + Apart from that we could just change state on the quote. */ + if (state == inName) + state = other; + else if (state == inValue && nAtts < attsMax) + atts[nAtts].normalized = 0; + break; + case BT_GT: + case BT_SOL: + if (state != inValue) + return nAtts; + break; + default: + break; + } + } + /* not reached */ +} + +static int +PREFIX(charRefNumber)(const ENCODING *enc, const char *ptr) { + int result = 0; + /* skip &# */ + UNUSED_P(enc); + ptr += 2 * MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_x)) { + for (ptr += MINBPC(enc); ! CHAR_MATCHES(enc, ptr, ASCII_SEMI); + ptr += MINBPC(enc)) { + int c = BYTE_TO_ASCII(enc, ptr); + switch (c) { + case ASCII_0: + case ASCII_1: + case ASCII_2: + case ASCII_3: + case ASCII_4: + case ASCII_5: + case ASCII_6: + case ASCII_7: + case ASCII_8: + case ASCII_9: + result <<= 4; + result |= (c - ASCII_0); + break; + case ASCII_A: + case ASCII_B: + case ASCII_C: + case ASCII_D: + case ASCII_E: + case ASCII_F: + result <<= 4; + result += 10 + (c - ASCII_A); + break; + case ASCII_a: + case ASCII_b: + case ASCII_c: + case ASCII_d: + case ASCII_e: + case ASCII_f: + result <<= 4; + result += 10 + (c - ASCII_a); + break; + } + if (result >= 0x110000) + return -1; + } + } else { + for (; ! CHAR_MATCHES(enc, ptr, ASCII_SEMI); ptr += MINBPC(enc)) { + int c = BYTE_TO_ASCII(enc, ptr); + result *= 10; + result += (c - ASCII_0); + if (result >= 0x110000) + return -1; + } + } + return checkCharRefNumber(result); +} + +static int +PREFIX(predefinedEntityName)(const ENCODING *enc, const char *ptr, + const char *end) { + UNUSED_P(enc); + switch ((end - ptr) / MINBPC(enc)) { + case 2: + if (CHAR_MATCHES(enc, ptr + MINBPC(enc), ASCII_t)) { + switch (BYTE_TO_ASCII(enc, ptr)) { + case ASCII_l: + return ASCII_LT; + case ASCII_g: + return ASCII_GT; + } + } + break; + case 3: + if (CHAR_MATCHES(enc, ptr, ASCII_a)) { + ptr += MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_m)) { + ptr += MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_p)) + return ASCII_AMP; + } + } + break; + case 4: + switch (BYTE_TO_ASCII(enc, ptr)) { + case ASCII_q: + ptr += MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_u)) { + ptr += MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_o)) { + ptr += MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_t)) + return ASCII_QUOT; + } + } + break; + case ASCII_a: + ptr += MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_p)) { + ptr += MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_o)) { + ptr += MINBPC(enc); + if (CHAR_MATCHES(enc, ptr, ASCII_s)) + return ASCII_APOS; + } + } + break; + } + } + return 0; +} + +static int +PREFIX(nameMatchesAscii)(const ENCODING *enc, const char *ptr1, + const char *end1, const char *ptr2) { + UNUSED_P(enc); + for (; *ptr2; ptr1 += MINBPC(enc), ptr2++) { + if (end1 - ptr1 < MINBPC(enc)) { + /* This line cannot be executed. The incoming data has already + * been tokenized once, so incomplete characters like this have + * already been eliminated from the input. Retaining the + * paranoia check is still valuable, however. + */ + return 0; /* LCOV_EXCL_LINE */ + } + if (! CHAR_MATCHES(enc, ptr1, *ptr2)) + return 0; + } + return ptr1 == end1; +} + +static int +PREFIX(nameLength)(const ENCODING *enc, const char *ptr) { + const char *start = ptr; + for (;;) { + switch (BYTE_TYPE(enc, ptr)) { +# define LEAD_CASE(n) \ + case BT_LEAD##n: \ + ptr += n; /* NOTE: The encoding has already been validated. */ \ + break; + LEAD_CASE(2) + LEAD_CASE(3) + LEAD_CASE(4) +# undef LEAD_CASE + case BT_NONASCII: + case BT_NMSTRT: +# ifdef XML_NS + case BT_COLON: +# endif + case BT_HEX: + case BT_DIGIT: + case BT_NAME: + case BT_MINUS: + ptr += MINBPC(enc); + break; + default: + return (int)(ptr - start); + } + } +} + +static const char * +PREFIX(skipS)(const ENCODING *enc, const char *ptr) { + for (;;) { + switch (BYTE_TYPE(enc, ptr)) { + case BT_LF: + case BT_CR: + case BT_S: + ptr += MINBPC(enc); + break; + default: + return ptr; + } + } +} + +static void +PREFIX(updatePosition)(const ENCODING *enc, const char *ptr, const char *end, + POSITION *pos) { + while (HAS_CHAR(enc, ptr, end)) { + switch (BYTE_TYPE(enc, ptr)) { +# define LEAD_CASE(n) \ + case BT_LEAD##n: \ + ptr += n; /* NOTE: The encoding has already been validated. */ \ + pos->columnNumber++; \ + break; + LEAD_CASE(2) + LEAD_CASE(3) + LEAD_CASE(4) +# undef LEAD_CASE + case BT_LF: + pos->columnNumber = 0; + pos->lineNumber++; + ptr += MINBPC(enc); + break; + case BT_CR: + pos->lineNumber++; + ptr += MINBPC(enc); + if (HAS_CHAR(enc, ptr, end) && BYTE_TYPE(enc, ptr) == BT_LF) + ptr += MINBPC(enc); + pos->columnNumber = 0; + break; + default: + ptr += MINBPC(enc); + pos->columnNumber++; + break; + } + } +} + +# undef DO_LEAD_CASE +# undef MULTIBYTE_CASES +# undef INVALID_CASES +# undef CHECK_NAME_CASE +# undef CHECK_NAME_CASES +# undef CHECK_NMSTRT_CASE +# undef CHECK_NMSTRT_CASES + +#endif /* XML_TOK_IMPL_C */ diff --git a/port/third_party/expat/xmltok_impl.h b/port/third_party/expat/xmltok_impl.h new file mode 100644 index 00000000..13a08899 --- /dev/null +++ b/port/third_party/expat/xmltok_impl.h @@ -0,0 +1,76 @@ +/* + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2017-2019 Sebastian Pipping + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +enum { + BT_NONXML, /* e.g. noncharacter-FFFF */ + BT_MALFORM, /* illegal, with regard to encoding */ + BT_LT, /* less than = "<" */ + BT_AMP, /* ampersand = "&" */ + BT_RSQB, /* right square bracket = "[" */ + BT_LEAD2, /* lead byte of a 2-byte UTF-8 character */ + BT_LEAD3, /* lead byte of a 3-byte UTF-8 character */ + BT_LEAD4, /* lead byte of a 4-byte UTF-8 character */ + BT_TRAIL, /* trailing unit, e.g. second 16-bit unit of a 4-byte char. */ + BT_CR, /* carriage return = "\r" */ + BT_LF, /* line feed = "\n" */ + BT_GT, /* greater than = ">" */ + BT_QUOT, /* quotation character = "\"" */ + BT_APOS, /* apostrophe = "'" */ + BT_EQUALS, /* equal sign = "=" */ + BT_QUEST, /* question mark = "?" */ + BT_EXCL, /* exclamation mark = "!" */ + BT_SOL, /* solidus, slash = "/" */ + BT_SEMI, /* semicolon = ";" */ + BT_NUM, /* number sign = "#" */ + BT_LSQB, /* left square bracket = "[" */ + BT_S, /* white space, e.g. "\t", " "[, "\r"] */ + BT_NMSTRT, /* non-hex name start letter = "G".."Z" + "g".."z" + "_" */ + BT_COLON, /* colon = ":" */ + BT_HEX, /* hex letter = "A".."F" + "a".."f" */ + BT_DIGIT, /* digit = "0".."9" */ + BT_NAME, /* dot and middle dot = "." + chr(0xb7) */ + BT_MINUS, /* minus = "-" */ + BT_OTHER, /* known not to be a name or name start character */ + BT_NONASCII, /* might be a name or name start character */ + BT_PERCNT, /* percent sign = "%" */ + BT_LPAR, /* left parenthesis = "(" */ + BT_RPAR, /* right parenthesis = "(" */ + BT_AST, /* asterisk = "*" */ + BT_PLUS, /* plus sign = "+" */ + BT_COMMA, /* comma = "," */ + BT_VERBAR /* vertical bar = "|" */ +}; + +#include diff --git a/port/third_party/expat/xmltok_ns.c b/port/third_party/expat/xmltok_ns.c new file mode 100644 index 00000000..d9f69abb --- /dev/null +++ b/port/third_party/expat/xmltok_ns.c @@ -0,0 +1,125 @@ +/* This file is included! + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| + | __// \| |_) | (_| | |_ + \___/_/\_\ .__/ \__,_|\__| + |_| XML parser + + Copyright (c) 1997-2000 Thai Open Source Software Center Ltd + Copyright (c) 2000 Clark Cooper + Copyright (c) 2002 Greg Stein + Copyright (c) 2002 Fred L. Drake, Jr. + Copyright (c) 2002-2006 Karl Waclawek + Copyright (c) 2017-2026 Sebastian Pipping + Copyright (c) 2025 Alfonso Gregory + Licensed under the MIT license: + + 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. + + SPDX-License-Identifier: MIT +*/ + +#ifdef XML_TOK_NS_C + +const ENCODING * +NS(XmlGetUtf8InternalEncoding)(void) { + return &ns(internal_utf8_encoding).enc; +} + +const ENCODING * +NS(XmlGetUtf16InternalEncoding)(void) { +# if BYTEORDER == 1234 + return &ns(internal_little2_encoding).enc; +# elif BYTEORDER == 4321 + return &ns(internal_big2_encoding).enc; +# else + const short n = 1; + return (*(const char *)&n ? &ns(internal_little2_encoding).enc + : &ns(internal_big2_encoding).enc); +# endif +} + +static const ENCODING *const NS(encodings)[] = { + &ns(latin1_encoding).enc, &ns(ascii_encoding).enc, + &ns(utf8_encoding).enc, &ns(big2_encoding).enc, + &ns(big2_encoding).enc, &ns(little2_encoding).enc, + &ns(utf8_encoding).enc /* NO_ENC */ +}; + +static int +NS(initScanProlog)(const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + return initScan(NS(encodings), (const INIT_ENCODING *)enc, XML_PROLOG_STATE, + ptr, end, nextTokPtr); +} + +static int +NS(initScanContent)(const ENCODING *enc, const char *ptr, const char *end, + const char **nextTokPtr) { + return initScan(NS(encodings), (const INIT_ENCODING *)enc, XML_CONTENT_STATE, + ptr, end, nextTokPtr); +} + +int +NS(XmlInitEncoding)(INIT_ENCODING *p, const ENCODING **encPtr, + const char *name) { + int i = getEncodingIndex(name); + if (i == UNKNOWN_ENC) + return 0; + SET_INIT_ENC_INDEX(p, i); + p->initEnc.scanners[XML_PROLOG_STATE] = NS(initScanProlog); + p->initEnc.scanners[XML_CONTENT_STATE] = NS(initScanContent); + p->initEnc.updatePosition = initUpdatePosition; + p->encPtr = encPtr; + *encPtr = &(p->initEnc); + return 1; +} + +static const ENCODING * +NS(findEncoding)(const ENCODING *enc, const char *ptr, const char *end) { +# define ENCODING_MAX 128 + char buf[ENCODING_MAX] = ""; + char *p = buf; + int i; + XmlUtf8Convert(enc, &ptr, end, &p, p + ENCODING_MAX - 1); + if (ptr != end) + return NULL; + *p = 0; + if (streqci(buf, KW_UTF_16) && enc->minBytesPerChar == 2) + return enc; + i = getEncodingIndex(buf); + if (i == UNKNOWN_ENC) + return NULL; + return NS(encodings)[i]; +} + +int +NS(XmlParseXmlDecl)(int isGeneralTextEntity, const ENCODING *enc, + const char *ptr, const char *end, const char **badPtr, + const char **versionPtr, const char **versionEndPtr, + const char **encodingName, const ENCODING **encoding, + int *standalone) { + return doParseXmlDecl(NS(findEncoding), isGeneralTextEntity, enc, ptr, end, + badPtr, versionPtr, versionEndPtr, encodingName, + encoding, standalone); +} + +#endif /* XML_TOK_NS_C */ diff --git a/source/cache/cache_files.c b/source/cache/cache_files.c index 264dae19..80d53d27 100644 --- a/source/cache/cache_files.c +++ b/source/cache/cache_files.c @@ -333,6 +333,14 @@ void scenario_tags_unload( } sound_cache_close(); texture_cache_close(); + /* port: the menus' tags go, and the map's own table comes back + (port/linux/game/menu_tags.c): after the texture cache, which writes to + the bitmaps it has loaded as it closes, theirs among them */ + { + extern void menu_tags_unloaded(void); + + menu_tags_unloaded(); + } cache_file_close(); tags_header_deregister_vertex_and_index_buffers(cache_file_globals.tag_header); cache_file_globals.tags_loaded = FALSE; @@ -341,6 +349,24 @@ void scenario_tags_unload( return; } +/* port: the loaded tags' table, and its tags' count, for the menus' tags +(port/linux/game/menu_tags.c), which a copy with theirs added replaces */ +void *cache_files_tag_instances( + long *count) +{ + *count = cache_file_globals.tags_loaded ? cache_file_globals.tag_header->tag_count : 0; + return cache_file_globals.tags_loaded ? cache_file_globals.tag_header->tag_instances : NULL; +} + +void cache_files_set_tag_instances( + void *instances, + long count) +{ + cache_file_globals.tag_header->tag_instances = instances; + cache_file_globals.tag_header->tag_count = count; + global_tag_instances = instances; +} + void tag_files_open( void) { @@ -805,6 +831,12 @@ long scenario_tags_load( pal_tags_loaded(cache_file_globals.header.build); } + /* port: the menus' tags, added to the map's (port/linux/game/menu_tags.c) */ + { + extern void menu_tags_loaded(char const *map_name); + + menu_tags_loaded(cache_file_globals.header.name); + } /* port: the bitmaps the high-res HUD stands for (port/linux/game/hud_hires_tags.c) */ { extern void hud_hires_tags_loaded(void); diff --git a/source/game/game.c b/source/game/game.c index e4a94e4d..dfcf04ea 100644 --- a/source/game/game.c +++ b/source/game/game.c @@ -231,6 +231,8 @@ typedef char verify_game_runtime_globals_difficulty_offset[ static struct game_runtime_globals_prefix *game_globals = NULL; extern struct game_variant game_variant_global; extern struct data_array *player_data; +/* port: the PC options the game plays by (game_set_game_variant_options) */ +static struct game_variant_options game_variant_options_global; char const *global_game_difficulty_level_names[NUMBER_OF_GAME_DIFFICULTY_LEVELS] = { @@ -428,10 +430,29 @@ void game_set_game_variant( { game_variant_global = *variant; } + /* port: its PC options, until the network game's are given */ + game_variant_options_default(&game_variant_global, &game_variant_options_global); return; } +void game_set_game_variant_options( + struct game_variant_options const *options) +{ + if (options) + game_variant_options_global = *options; + else + game_variant_options_default(&game_variant_global, &game_variant_options_global); + + return; +} + +struct game_variant_options const *game_variant_options_get( + void) +{ + return &game_variant_options_global; +} + void game_set_game_engine_index( short index) { diff --git a/source/game/game.h b/source/game/game.h index fca81902..a20d2a9b 100644 --- a/source/game/game.h +++ b/source/game/game.h @@ -150,6 +150,11 @@ void game_difficulty_level_set(short difficulty); short game_difficulty_level_get(void); short game_difficulty_level_get_ignore_easy(void); void game_set_game_variant(struct game_variant *variant); +/* port: the PC options the game plays by (game_engine.h): the variant's +defaults once it is set, then the network game's */ +struct game_variant_options; +void game_set_game_variant_options(struct game_variant_options const *options); +struct game_variant_options const *game_variant_options_get(void); void game_set_game_engine_index(short index); boolean game_all_quiet( void); diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 1bf8d89e..f4fad73e 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -567,6 +567,9 @@ symbols in this file: #include "math/integer_math.h" #include "networking/network_game_globals.h" #include "networking/network_server_manager.h" +#include "networking/network_game_manager.h" +/* (network_server_manager_internal.h's: the host's game record) */ +struct network_game *network_game_server_get_game(struct network_game_server *server); #include "objects.h" #include "objects/damage_effect_definitions.h" #include "physics/collision_features.h" @@ -620,11 +623,6 @@ enum _game_engine_game_over_sound_disabled_bit, }; -enum -{ - _game_variant_generic_starting_equipment_bit = 5, -}; - enum { _game_engine_all = 12, @@ -676,6 +674,10 @@ enum game_engine_weapons _game_engine_weapons_short_range, _game_engine_weapons_human, _game_engine_weapons_no_grenades, + /* port: the PC version's */ + _game_engine_weapons_covenant, + _game_engine_weapons_classic, + _game_engine_weapons_heavy, NUMBER_OF_GAME_ENGINE_WEAPON_SETS, }; @@ -1078,6 +1080,7 @@ static void game_engine_generate_title_string( { wchar_t *secondary_string; wchar_t life_string[128]; + wchar_t time_left_string[160]; wchar_t score_string[256]; long string_list_index; wchar_t *format_string; @@ -1145,6 +1148,24 @@ static void game_engine_generate_title_string( } } + /* port: the gametype's time limit's time left (game_variant_options) */ + if (game_variant_options_get()->time_limit > 0 && + game_engine_globals.postgame_state == game_engine_mode_active) + { + long left = game_variant_options_get()->time_limit * 60L * TICKS_PER_SECOND - game_time_get(); + wchar_t time_string[32]; + + ticks_to_unicode_time_string(MAX(left, 0), NUMBEROF(time_string), time_string); + if (secondary_string[0] && secondary_string != time_left_string) + { + usnprintf(time_left_string, NUMBEROF(time_left_string), L"%s, %s left", secondary_string, time_string); + } + else + usnprintf(time_left_string, NUMBEROF(time_left_string), L"%s left", time_string); + time_left_string[NUMBEROF(time_left_string) - 1] = 0; + secondary_string = time_left_string; + } + if (game_engine_globals.postgame_state == game_engine_mode_postgame_delay) { long did_player_win = game_engine_did_player_win(player_index); @@ -1677,6 +1698,7 @@ long distributed_player_ping(short player_index); /* port_config.c's */ int config_boolean(const char *name); const char *config_string(const char *name); +unsigned long config_changes(void); /* cinematics.c's */ void draw_quad(rectangle2d *rectangle, pixel32 color); @@ -1685,16 +1707,18 @@ text, its colour "red, green, blue, alpha" (0 to 255 each), or 0 for none */ static pixel32 scoreboard_background_color( void) { - static boolean read = FALSE; + static unsigned long read_at = (unsigned long)-1; static pixel32 color = 0; - if (!read) + /* (read again when Settings changes it) */ + if (read_at != config_changes()) { char const *text = config_string("display.scoreboard_background_color"); long parts[4] = { 16, 16, 16, 150 }; short part; - read = TRUE; + read_at = config_changes(); + color = 0; if (!config_boolean("display.scoreboard_background")) return color; for (part = 0; part < 4 && text && *text; part++) @@ -1785,11 +1809,13 @@ static boolean scoreboard_team_columns( void) { static short setting = NONE; + static unsigned long read_at = (unsigned long)-1; - if (setting == NONE) + if (read_at != config_changes()) { char const *value = config_string("display.scoreboard_team_layout"); + read_at = config_changes(); setting = value && !csstrcmp(value, "score") ? FALSE : TRUE; } @@ -3602,6 +3628,24 @@ boolean game_engine_showing_postgame( return game_engine != NULL && game_engine_globals.postgame_state == game_engine_mode_postgame_rasterize; } +void game_variant_options_default( + struct game_variant const *variant, + struct game_variant_options *options) +{ + csmemset(options, 0, sizeof(*options)); + options->friendly_fire = _friendly_fire_on; + options->loadout = _loadout_category; + options->primary_weapon = _loadout_weapon_assault_rifle; + options->secondary_weapon = _loadout_weapon_pistol; + options->radar_players = variant && + !TEST_FLAG(variant->universal_variant.flags, _game_variant_draw_object_in_motion_sensor_bit) ? + _radar_players_none : _radar_players_all; + options->vehicle_set[0] = options->vehicle_set[1] = + variant ? (byte)variant->universal_variant.vehicle_set : 0; + + return; +} + boolean game_engine_force_single_screen( void) { @@ -4075,6 +4119,99 @@ static void game_engine_update_teleporter( return; } +/* port: the gametype's vehicle respawn time (game_variant_options): the +host puts a vehicle destroyed, or left empty away from where the map put +it, back there after the time (the clients get the host's: the distributed +netcode). Vehicles otherwise stay where they are left (the Xbox game's). */ +enum +{ + MAXIMUM_VEHICLE_HOMES = 64 +}; + +static struct +{ + long object_index; + long definition_index; + real_point3d position; + real_vector3d forward; + real_vector3d up; + long idle_since; +} game_engine_vehicle_homes[MAXIMUM_VEHICLE_HOMES]; +static short game_engine_vehicle_home_count = NONE; + +static void game_engine_update_vehicle_respawn( + void) +{ + long respawn_ticks = game_variant_options_get()->vehicle_respawn_time * TICKS_PER_SECOND; + long now = game_time_get(); + short index; + + if (respawn_ticks <= 0 || network_game_distributed_client() || now % TICKS_PER_SECOND) + return; + /* (where the map put them: the vehicles there at the first look) */ + if (game_engine_vehicle_home_count == NONE) + { + struct object_iterator iterator; + + game_engine_vehicle_home_count = 0; + object_iterator_new(&iterator, _object_mask_vehicle, 0); + while (object_iterator_next(&iterator) && game_engine_vehicle_home_count < MAXIMUM_VEHICLE_HOMES) + { + struct object_datum *object = object_get(iterator.index); + short home = game_engine_vehicle_home_count++; + + game_engine_vehicle_homes[home].object_index = iterator.index; + game_engine_vehicle_homes[home].definition_index = object->definition_index; + game_engine_vehicle_homes[home].position = object->object.position; + object_get_orientation(iterator.index, &game_engine_vehicle_homes[home].forward, + &game_engine_vehicle_homes[home].up); + game_engine_vehicle_homes[home].idle_since = NONE; + } + } + for (index = 0; index < game_engine_vehicle_home_count; index++) + { + struct unit_datum *vehicle = unit_try_and_get(game_engine_vehicle_homes[index].object_index); + boolean waiting = TRUE; + + if (vehicle && !TEST_FLAG(vehicle->object.damage_flags, _object_dead_bit)) + { + real dx = vehicle->object.position.x - game_engine_vehicle_homes[index].position.x; + real dy = vehicle->object.position.y - game_engine_vehicle_homes[index].position.y; + real dz = vehicle->object.position.z - game_engine_vehicle_homes[index].position.z; + + long child_index; + + /* (empty: no rider among its children, which hold its own + weapons too) */ + waiting = vehicle->unit.driver_object_index == NONE && vehicle->unit.gunner_object_index == NONE && + dx * dx + dy * dy + dz * dz > 4.0f; + for (child_index = vehicle->object.first_child_object_index; waiting && child_index != NONE; + child_index = object_get(child_index)->object.next_object_index) + { + if (object_get(child_index)->object.type == _object_type_biped) + waiting = FALSE; + } + } + if (!waiting) + game_engine_vehicle_homes[index].idle_since = NONE; + else if (game_engine_vehicle_homes[index].idle_since == NONE) + game_engine_vehicle_homes[index].idle_since = now; + else if (now - game_engine_vehicle_homes[index].idle_since >= respawn_ticks) + { + struct object_placement_data placement_data; + + if (vehicle) + object_delete(game_engine_vehicle_homes[index].object_index); + object_placement_data_new(&placement_data, game_engine_vehicle_homes[index].definition_index, NONE); + placement_data.position = game_engine_vehicle_homes[index].position; + placement_data.forward = game_engine_vehicle_homes[index].forward; + placement_data.up = game_engine_vehicle_homes[index].up; + game_engine_vehicle_homes[index].object_index = object_new(&placement_data); + game_engine_vehicle_homes[index].idle_since = NONE; + } + } +} + void game_engine_update( void) { @@ -4084,6 +4221,7 @@ void game_engine_update( game_engine_update_purge(); game_engine_update_weapons(); game_engine_update_item_spawn(); + game_engine_update_vehicle_respawn(); { struct data_iterator player_iterator; @@ -4234,6 +4372,10 @@ enum _equipment_created_at_rest_bit = 0, }; +/* port: each player's friendly fire penalty (game_variant_options), added to +his next respawn: by the player's absolute index */ +static short game_engine_betrayal_penalty[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; + void game_engine_player_killed( long killing_player_index, long killing_object_index, @@ -4304,6 +4446,56 @@ void game_engine_player_killed( if (!player_kill) dead_player->respawn_timer += global_variant.universal_variant.suicide_penalty; + /* port: the gametype's auto team balance (game_variant_options): the host + moves the dead to the other team when his is two or more bigger (his + next unit is the other team's, which the clients take it from: + network_player_attach_unit) */ + if (global_variant.universal_variant.teams && game_variant_options_get()->auto_team_balance && + !network_game_distributed_client() && VALID_INDEX(dead_player->team_index, 2) && !dead_player->quit_out_of_game) + { + struct data_iterator iterator; + struct player_datum *player; + short count[2] = { 0, 0 }; + + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) + { + if (!player->quit_out_of_game && VALID_INDEX(player->team_index, 2)) + count[player->team_index]++; + } + if (count[dead_player->team_index] >= count[1 - dead_player->team_index] + 2) + { + struct network_game_server *server = global_network_game_server_get(); + struct network_game *game = server ? network_game_server_get_game(server) : NULL; + long list_index = dead_player->network_player_data.player_list_index; + + dead_player->team_index = 1 - dead_player->team_index; + dead_player->network_player_data.team_index = (char)dead_player->team_index; + /* (and the host's game record, which late joins count and the + lobby shows) */ + if (game && VALID_INDEX(list_index, HALO_PORT_MAXIMUM_NETWORK_PLAYERS)) + game->players[list_index].team_index = (char)dead_player->team_index; + } + } + + /* port: the gametype's friendly fire penalty: the team killer's next + respawn later (the dead's own, if he has one waiting) */ + { + long dead_absolute = DATUM_INDEX_TO_ABSOLUTE_INDEX(dead_player_index); + + if (dead_absolute < HALO_PORT_MAXIMUM_NETWORK_PLAYERS) + { + dead_player->respawn_timer += game_engine_betrayal_penalty[dead_absolute]; + game_engine_betrayal_penalty[dead_absolute] = 0; + } + if (friendly_fire && valid_players && !same_player && global_variant.universal_variant.teams && + DATUM_INDEX_TO_ABSOLUTE_INDEX(killing_player_index) < HALO_PORT_MAXIMUM_NETWORK_PLAYERS) + { + game_engine_betrayal_penalty[DATUM_INDEX_TO_ABSOLUTE_INDEX(killing_player_index)] = + (short)(game_variant_options_get()->friendly_fire_penalty * TICKS_PER_SECOND); + } + } + dead_player->respawn_timer = MAX(dead_player->respawn_timer, 90); dead_player = player_get(dead_player_index); @@ -4492,6 +4684,16 @@ boolean game_engine_has_shield( return has_shield; } +/* port: the local player whose motion sensor is drawn (motion_sensor.c), +for the gametype's FRIENDS radar (game_variant_options) */ +static short game_engine_motion_sensor_local_player = NONE; + +void game_engine_motion_sensor_viewer( + short local_player_index) +{ + game_engine_motion_sensor_local_player = local_player_index; +} + boolean game_engine_draw_object_in_motion_sensor( long unit_index) { @@ -4499,6 +4701,16 @@ boolean game_engine_draw_object_in_motion_sensor( if (game_engine) draw_object = TEST_FLAG(global_variant.universal_variant.flags, _game_variant_draw_object_in_motion_sensor_bit); + /* port: friends only: the viewer's team's */ + if (draw_object && game_engine && global_variant.universal_variant.teams && + game_variant_options_get()->radar_players == _radar_players_friends && + game_engine_motion_sensor_local_player != NONE) + { + long player_index = local_player_get_player_index(game_engine_motion_sensor_local_player); + struct player_datum *viewer = player_index != NONE ? player_try_and_get(player_index) : NULL; + + draw_object = viewer && object_get(unit_index)->object.owner_team_index == viewer->team_index; + } return draw_object; } @@ -4759,6 +4971,12 @@ boolean game_engine_should_end_game( if (game_engine && !multiple_teams_alive()) should_end_game = TRUE; + /* port: the gametype's time limit (game_variant_options) */ + if (game_engine && game_variant_options_get()->time_limit > 0 && + game_time_get() >= game_variant_options_get()->time_limit * 60L * TICKS_PER_SECOND) + { + should_end_game = TRUE; + } return should_end_game; } @@ -6567,6 +6785,8 @@ void game_engine_initialize_for_new_map( game_engine_intialize_queued_sounds(); csmemset(global_goal, 0, sizeof(global_goal)); game_engine_globals.next_team_index = 0; + csmemset(game_engine_betrayal_penalty, 0, sizeof(game_engine_betrayal_penalty)); + game_engine_vehicle_home_count = NONE; timeout_for_endgame_sound = 0; game_engine_network_state_read = FALSE; @@ -6596,6 +6816,9 @@ void game_engine_player_added( long player_index) { initialize_player_multiplayer_data(player_index); + /* port: not the friendly fire penalty of the slot's last player */ + if (DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) < HALO_PORT_MAXIMUM_NETWORK_PLAYERS) + game_engine_betrayal_penalty[DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index)] = 0; if (game_engine) { @@ -6713,6 +6936,146 @@ real game_engine_get_starting_location_rating( return default_starting_location_rate_function(player_index, starting_location); } +/* port: the gametype's vehicles of each team (game_variant_options) */ +static byte game_engine_vehicles_placed[2][NUMBER_OF_VARIANT_VEHICLES]; + +void game_engine_vehicle_placement_begin( + void) +{ + csmemset(game_engine_vehicles_placed, 0, sizeof(game_engine_vehicles_placed)); +} + +/* a vehicle definition's type: the globals' multiplayer vehicles (warthog, +ghost, scorpion), else by its tag's name; NONE for none of them */ +static short game_engine_variant_vehicle_type( + long definition_index) +{ + struct game_globals *game_globals = scenario_get_game_globals(); + struct game_globals_multiplayer_information *information; + char const *name; + short index; + + if (definition_index == NONE) + return NONE; + if (game_globals->multiplayer_information.count > 0) + { + information = TAG_BLOCK_GET_ELEMENT(&game_globals->multiplayer_information, 0, + struct game_globals_multiplayer_information); + for (index = 0; index < 3 && index < information->vehicles.count; index++) + { + if (TAG_BLOCK_GET_ELEMENT(&information->vehicles, index, struct game_globals_vehicle)->vehicle.index == + definition_index) + { + return index == 0 ? _variant_vehicle_warthog : index == 1 ? _variant_vehicle_ghost : + _variant_vehicle_scorpion; + } + } + } + name = tag_get_name(definition_index); + if (!name) + return NONE; + if (strstr(name, "rwarthog") || strstr(name, "rocket")) + return _variant_vehicle_rocket_warthog; + if (strstr(name, "banshee")) + return _variant_vehicle_banshee; + if (strstr(name, "turret")) + return _variant_vehicle_gun_turret; + if (strstr(name, "warthog")) + return _variant_vehicle_warthog; + if (strstr(name, "ghost")) + return _variant_vehicle_ghost; + if (strstr(name, "scorpion")) + return _variant_vehicle_scorpion; + return NONE; +} + +/* the team whose spawn points are nearest the point (red's if none) */ +static short game_engine_nearest_team( + real_point3d const *point) +{ + struct scenario *scenario = global_scenario_get(); + real best = -1.0f; + short team = 0; + long index; + + for (index = 0; index < scenario->players.count; index++) + { + struct player_starting_location *location = + TAG_BLOCK_GET_ELEMENT(&scenario->players, index, struct player_starting_location); + real dx = location->position.x - point->x; + real dy = location->position.y - point->y; + real dz = location->position.z - point->z; + real distance = dx * dx + dy * dy + dz * dz; + + if (VALID_INDEX(location->team_index, 2) && (best < 0.0f || distance < best)) + { + best = distance; + team = location->team_index; + } + } + return team; +} + +boolean game_engine_vehicle_placement_allowed( + struct scenario_object_datum const *placement, + struct tag_block *palette) +{ + struct game_variant_options const *options = game_variant_options_get(); + short side, type; + byte set; + + if (!game_engine || placement->palette_entry_index == NONE) + return TRUE; + side = global_variant.universal_variant.teams ? game_engine_nearest_team(&placement->position) : 0; + set = options->vehicle_set[side]; + type = game_engine_variant_vehicle_type(TAG_BLOCK_GET_ELEMENT(palette, placement->palette_entry_index, + struct scenario_object_palette_entry)->reference.index); + /* (the map's own: the multiplayer ones of the globals, as the Xbox + game's remap allows) */ + if (set == _game_engine_vehicles_default) + return type == NONE || type <= _variant_vehicle_scorpion; + if (type == NONE) + return set != _game_engine_vehicles_none; + if (set == VARIANT_VEHICLE_SET_CUSTOM) + { + if (game_engine_vehicles_placed[side][type] >= options->vehicle_counts[side][type]) + return FALSE; + game_engine_vehicles_placed[side][type]++; + return TRUE; + } + /* (the sets: none, then each type alone, in the options' order) */ + return set != _game_engine_vehicles_none && set - _game_engine_vehicles_warthog == type; +} + +/* port: the gametype's friendly fire (game_variant_options) on a teammate's +hit of the object (its damage marked friendly: object_cause_damage): all of +it, none, its shields only, or explosives' only. A player's own hit of +himself, or of what is not a player's teammate's, is the game's own. */ +short game_engine_friendly_damage( + long attacker_player_index, + long object_index, + boolean explosive) +{ + struct game_variant_options const *options = game_variant_options_get(); + struct player_datum *attacker; + + if (!game_engine || !global_variant.universal_variant.teams || attacker_player_index == NONE || + options->friendly_fire == _friendly_fire_on) + { + return _friendly_damage_all; + } + attacker = (struct player_datum *)datum_try_and_get(player_data, attacker_player_index); + if (!attacker || attacker->unit_index == object_index) + return _friendly_damage_all; + switch (options->friendly_fire) + { + case _friendly_fire_off: return _friendly_damage_none; + case _friendly_fire_shields_only: return _friendly_damage_shields; + case _friendly_fire_explosives_only: return explosive ? _friendly_damage_all : _friendly_damage_none; + } + return _friendly_damage_all; +} + real game_engine_get_damage_multiplier( long damaging_player_index, long damaged_player_index) @@ -6767,14 +7130,22 @@ long game_engine_remap_vehicle( vehicle1 = TAG_BLOCK_GET_ELEMENT(vehicles, 1, struct game_globals_vehicle); vehicle2 = TAG_BLOCK_GET_ELEMENT(vehicles, 2, struct game_globals_vehicle); + /* (port: and the other types a gametype's sets name, which a map may + have: game_engine_variant_vehicle_type) */ if (result != vehicle0->vehicle.index && result != vehicle1->vehicle.index && - result != vehicle2->vehicle.index) + result != vehicle2->vehicle.index && + ((game_variant_options_get()->vehicle_set[0] == _game_engine_vehicles_default && + game_variant_options_get()->vehicle_set[1] == _game_engine_vehicles_default) || + game_engine_variant_vehicle_type(result) == NONE)) { result = NONE; } - switch (global_variant.universal_variant.vehicle_set) + /* port: the per-team sets decide at placement + (game_engine_vehicle_placement_allowed) */ + switch (game_variant_options_get()->vehicle_set[0] == game_variant_options_get()->vehicle_set[1] ? + global_variant.universal_variant.vehicle_set : _game_engine_vehicles_default) { case _game_engine_vehicles_none: result = NONE; @@ -6825,6 +7196,10 @@ long game_engine_remap_weapon( return weapon_definition_index; } + /* port: a custom loadout (game_variant_options) has no weapon set */ + if (game_variant_options_get()->loadout == _loadout_custom) + return weapon_definition_index; + if (weapon_list_index == _weapon_list_flamethrower) weapon_list_index = _weapon_list_rocket_launcher; @@ -6922,6 +7297,47 @@ long game_engine_remap_weapon( break; } break; + + /* port: the PC version's sets. Covenant: the Covenant's weapons (the + fuel rod, where the map has one, for the rocket launcher) */ + case _game_engine_weapons_covenant: + switch (weapon_list_index) + { + case _weapon_list_assault_rifle: + weapon_list_index = _weapon_list_plasma_rifle; + break; + + case _weapon_list_pistol: + weapon_list_index = _weapon_list_plasma_pistol; + break; + + case _weapon_list_shotgun: + case _weapon_list_sniper_rifle: + weapon_list_index = _weapon_list_needler; + break; + + case _weapon_list_rocket_launcher: + weapon_list_index = list_index_to_weapon_definition_index(_weapon_list_gravity_rifle) != NONE ? + _weapon_list_gravity_rifle : _weapon_list_needler; + break; + } + break; + + /* classic: the Xbox game's weapons (no fuel rod: the flamethrower is + already a rocket launcher) */ + case _game_engine_weapons_classic: + if (weapon_list_index == _weapon_list_gravity_rifle) + weapon_list_index = _weapon_list_rocket_launcher; + break; + + /* heavy weapons: rocket launchers, and fuel rods where the map has them */ + case _game_engine_weapons_heavy: + if (weapon_list_index != _weapon_list_gravity_rifle || + list_index_to_weapon_definition_index(_weapon_list_gravity_rifle) == NONE) + { + weapon_list_index = _weapon_list_rocket_launcher; + } + break; } return list_index_to_weapon_definition_index(weapon_list_index); @@ -7987,6 +8403,71 @@ static void handle_custom_starting_equipment( return; } +/* port: a custom loadout's weapon's place in the globals' weapon list +(game_engine_h's _loadout_weapon_*, from the assault rifle); random: one +of those the map has */ +static long game_engine_loadout_weapon_definition( + byte weapon) +{ + static short const list_indices[] = + { + _weapon_list_assault_rifle, _weapon_list_pistol, _weapon_list_shotgun, _weapon_list_sniper_rifle, + _weapon_list_rocket_launcher, _weapon_list_plasma_pistol, _weapon_list_plasma_rifle, _weapon_list_needler, + _weapon_list_gravity_rifle, _weapon_list_flamethrower + }; + short index; + + if (weapon == _loadout_weapon_random) + { + long available[NUMBEROF(list_indices)]; + short count = 0; + + for (index = 0; index < NUMBEROF(list_indices); index++) + { + long definition_index = list_index_to_weapon_definition_index(list_indices[index]); + + if (definition_index != NONE) + available[count++] = definition_index; + } + return count ? available[local_random_range(0, count)] : NONE; + } + index = (short)(weapon - _loadout_weapon_assault_rifle); + return index >= 0 && index < NUMBEROF(list_indices) ? list_index_to_weapon_definition_index(list_indices[index]) : + NONE; +} + +/* port: the unit's weapons the loadout's (its others gone): the primary, +then the secondary */ +static void game_engine_give_loadout( + long unit_index) +{ + struct game_variant_options const *options = game_variant_options_get(); + byte const weapons[2] = { options->primary_weapon, options->secondary_weapon }; + boolean first = TRUE; + short index; + + unit_delete_all_weapons(unit_index); + for (index = 0; index < 2; index++) + { + long definition_index = game_engine_loadout_weapon_definition(weapons[index]); + struct object_placement_data placement_data; + long weapon_index; + + if (definition_index == NONE) + continue; + object_placement_data_new(&placement_data, definition_index, NONE); + weapon_index = object_new(&placement_data); + if (weapon_index == NONE) + continue; + if (!first && unit_has_weapon_definition_index(unit_index, definition_index)) + object_delete(weapon_index); + else if (unit_add_weapon_to_inventory(unit_index, weapon_index, first ? 2 : 0)) + first = FALSE; + else + object_delete(weapon_index); + } +} + void game_engine_postspawn_player_update( long player_index) { @@ -8035,7 +8516,12 @@ void game_engine_postspawn_player_update( fragmentation_grenade_count; long starting_plasma_grenade_count = 0; - if (!TEST_FLAG(global_variant.universal_variant.flags, _game_variant_generic_starting_equipment_bit)) + /* port: a custom loadout's weapons (game_variant_options) */ + if (game_variant_options_get()->loadout == _loadout_custom) + { + game_engine_give_loadout(unit_index); + } + else if (!TEST_FLAG(global_variant.universal_variant.flags, _game_variant_generic_starting_equipment_bit)) { handle_custom_starting_equipment( unit_index, diff --git a/source/game/game_engine.h b/source/game/game_engine.h index 192e6c7d..f15a1f1d 100644 --- a/source/game/game_engine.h +++ b/source/game/game_engine.h @@ -26,6 +26,8 @@ enum _game_variant_infinite_grenades_bit, _game_variant_no_shields_bit, _game_variant_always_invisible_bit, + /* (the starting equipment: generic, else the map's) */ + _game_variant_generic_starting_equipment_bit, }; enum game_engine_type @@ -147,6 +149,95 @@ struct game_variant typedef char verify_game_variant_size[sizeof(struct game_variant) == 0x68 ? 1 : -1]; +/* port: the PC version's gametype options that the Xbox's variant has not +(its gametype editor's, port/linux/game/menu_functions.c). A saved gametype +keeps them after its signature (playlist_profile.c), the network game +carries them (network_game_manager.h), and the game plays by them +(game_variant_options_get). */ +enum +{ + _friendly_fire_on = 0, + _friendly_fire_off, + _friendly_fire_shields_only, + _friendly_fire_explosives_only, + NUMBER_OF_FRIENDLY_FIRE_MODES +}; + +enum +{ + /* other players on the motion tracker: all (the variant's draw object + in motion sensor bit set), friends only, none */ + _radar_players_all = 0, + _radar_players_friends, + _radar_players_none, + NUMBER_OF_RADAR_PLAYERS +}; + +enum +{ + _loadout_category = 0, + _loadout_custom, + NUMBER_OF_LOADOUTS +}; + +enum +{ + _loadout_weapon_none = 0, + _loadout_weapon_random, + _loadout_weapon_assault_rifle, + _loadout_weapon_pistol, + _loadout_weapon_shotgun, + _loadout_weapon_sniper_rifle, + _loadout_weapon_rocket_launcher, + _loadout_weapon_plasma_pistol, + _loadout_weapon_plasma_rifle, + _loadout_weapon_needler, + _loadout_weapon_fuel_rod, + _loadout_weapon_flamethrower, + NUMBER_OF_LOADOUT_WEAPONS +}; + +enum +{ + _variant_vehicle_warthog = 0, + _variant_vehicle_ghost, + _variant_vehicle_scorpion, + _variant_vehicle_rocket_warthog, + _variant_vehicle_banshee, + _variant_vehicle_gun_turret, + NUMBER_OF_VARIANT_VEHICLES, + MAXIMUM_VARIANT_VEHICLE_COUNT = 4, + /* a team's vehicles: those of a vehicle set (universal_variant's + vehicle_set values), else its counts */ + VARIANT_VEHICLE_SET_CUSTOM = 0xFF +}; + +/* (the starting equipment, generic or the map's, is the variant's own: +_game_variant_generic_starting_equipment_bit) */ +struct game_variant_options +{ + /* minutes; 0: none */ + short time_limit; + short friendly_fire; + /* seconds added to a team killer's respawn */ + short friendly_fire_penalty; + /* seconds; 0: never (the Xbox game's) */ + short vehicle_respawn_time; + boolean auto_team_balance; + byte radar_players; + /* red's and blue's (free for all: red's) */ + byte vehicle_set[2]; + byte vehicle_counts[2][NUMBER_OF_VARIANT_VEHICLES]; + /* the weapons: the weapon set's (category), else each player starts + with these (custom: _loadout_weapon_none, _random, or a weapon) */ + byte loadout; + byte primary_weapon; + byte secondary_weapon; + byte pad; +}; + +typedef char verify_game_variant_options_size[sizeof(struct game_variant_options) == 0x1C ? 1 : -1]; + struct game_engine { char const *name; @@ -245,6 +336,35 @@ less the postgame state game_engine_write_network_state puts first) */ /* ---------- prototypes/GAME_ENGINE.C */ +/* port: the gametype's vehicles of each team (game_variant_options): the +map's placement of a vehicle allowed, by its type and the nearer team's +spawn points (counted from game_engine_vehicle_placement_begin) */ +struct scenario_object_datum; +struct tag_block; +void game_engine_vehicle_placement_begin( + void); +boolean game_engine_vehicle_placement_allowed( + struct scenario_object_datum const *placement, + struct tag_block *palette); +/* port: the local player whose motion sensor is drawn (FRIENDS radar) */ +void game_engine_motion_sensor_viewer( + short local_player_index); +/* port: the gametype's friendly fire on a teammate's hit of the object */ +enum +{ + _friendly_damage_all = 0, + _friendly_damage_none, + _friendly_damage_shields +}; +short game_engine_friendly_damage( + long attacker_player_index, + long object_index, + boolean explosive); +/* port: the PC options of a variant that has none of its own (saved before +them, or built in): the Xbox game's play */ +void game_variant_options_default( + struct game_variant const *variant, + struct game_variant_options *options); void game_engine_playlist_initialize( void); diff --git a/source/game/player_control.c b/source/game/player_control.c index 7e04d105..72798d4b 100644 --- a/source/game/player_control.c +++ b/source/game/player_control.c @@ -1387,6 +1387,20 @@ static void get_local_player_input_blob( input->unit_control_flags, _unit_control_action_bit, effective_buttons[_button_action_reload]); + /* port: the keyboard's reload key (port/linux/include/ + halo_keyboard.h), which the controller's X shares with the + action */ + if (!TEST_FLAG(control->inhibited_button_bit_vector, _button_action_reload) && + input_abstraction_port_reload(gamepad_index)) + { + SET_FLAG(input->unit_control_flags, _unit_control_weapon_reload_bit, TRUE); + } + /* port: and its action key acts only, never reloading */ + if (TEST_FLAG(input->unit_control_flags, _unit_control_action_bit) && + input_abstraction_port_action_only(gamepad_index)) + { + SET_FLAG(input->unit_control_flags, UNIT_CONTROL_PORT_ACTION_ONLY_BIT, TRUE); + } SET_FLAG( input->unit_control_flags, _unit_control_swap_weapons_bit, @@ -1424,6 +1438,9 @@ static void get_local_player_input_blob( _button_action_reload)) { input->accept = gamepad->buttons[_gamepad_analog_button_a]; + /* port: and the keyboard's jump key */ + if (!input->accept) + input->accept = input_abstraction_port_accept(gamepad_index); } } } diff --git a/source/game/player_queues_new.c b/source/game/player_queues_new.c index 9fec6305..2f66d350 100644 --- a/source/game/player_queues_new.c +++ b/source/game/player_queues_new.c @@ -137,7 +137,9 @@ enum FLAG(_unit_control_integrated_light_bit) | FLAG(_unit_control_action_bit) | FLAG(_unit_control_use_equipment_bit) | - FLAG(_unit_control_weapon_reload_bit), + FLAG(_unit_control_weapon_reload_bit) | + /* port: latched with the action it goes with (units.h) */ + FLAG(UNIT_CONTROL_PORT_ACTION_ONLY_BIT), }; /* ---------- macros */ diff --git a/source/game/players.c b/source/game/players.c index d925c94f..0376c715 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -1434,6 +1434,10 @@ void network_player_attach_unit( struct player_datum *player = player_get(player_index); struct unit_datum *unit = unit_get(unit_index); + /* port: the host's team for the player (auto team balance moves a + player to the other team at his death: game_engine_player_killed) */ + if (game_engine_has_teams() && unit->object.owner_team_index >= 0 && unit->object.owner_team_index < 2) + player->team_index = unit->object.owner_team_index; unit->object.owner_player_index = player_index; unit->object.owner_team_index = (short)player->team_index; unit->unit.player_index = player_index; @@ -3703,9 +3707,12 @@ void players_update_before_game( if (!players_globals->input_disabled) { network_player_log_idle_action(iterator.datum_index, action->control_flags); + /* port: not for the keyboard's action key, which only acts + (units.h, UNIT_CONTROL_PORT_ACTION_ONLY_BIT) */ if (TEST_FLAG(action->control_flags, _unit_control_action_bit) && unit->object.parent_object_index == NONE && - !player_handle_action(iterator.datum_index)) + !player_handle_action(iterator.datum_index) && + !TEST_FLAG(action->control_flags, UNIT_CONTROL_PORT_ACTION_ONLY_BIT)) { SET_FLAG(action->control_flags, _unit_control_weapon_reload_bit, TRUE); } @@ -3743,7 +3750,8 @@ void players_update_before_game( action->desired_weapon_index = unit->unit.current_weapon_index; } - control_data.control_flags = (word)action->control_flags; + control_data.control_flags = + (word)(action->control_flags & ~FLAG(UNIT_CONTROL_PORT_ACTION_ONLY_BIT)); player_aiming_vector_from_facing( iterator.datum_index, &control_data.aiming_vector, diff --git a/source/input/input_abstraction.c b/source/input/input_abstraction.c index bb07a829..a9decc9c 100644 --- a/source/input/input_abstraction.c +++ b/source/input/input_abstraction.c @@ -68,6 +68,7 @@ symbols in this file: #include "scenario/scenario.h" #include "units/vehicle_definitions.h" #include "units/vehicles.h" +#include "halo_keyboard.h" /* port */ /* ---------- constants */ @@ -180,6 +181,120 @@ static real const stick_direction_angles[] = -STICK_SECOND_QUADRANT_DIAGONAL_ANGLE, }; +/* port: the keyboard and mouse's own controls (port/linux/include/ +halo_keyboard.h): the actions held, taken as the controller's game controls +(held for as many ticks as input_xbox.c counts a button), its movement, and +reloading; Start and Back they press on the controller itself */ +static signed char const keyboard_game_controls[NUMBER_OF_GAME_CONTROLS] = +{ + HALO_KEYBOARD_JUMP, + HALO_KEYBOARD_SWITCH_GRENADE, + HALO_KEYBOARD_ACTION, + HALO_KEYBOARD_SWITCH_WEAPON, + HALO_KEYBOARD_MELEE, + HALO_KEYBOARD_FLASHLIGHT, + HALO_KEYBOARD_THROW_GRENADE, + HALO_KEYBOARD_FIRE, + -1, + -1, + HALO_KEYBOARD_CROUCH, + HALO_KEYBOARD_ZOOM, +}; + +static struct +{ + byte ticks[NUMBER_OF_GAME_CONTROLS]; + long down_times[NUMBER_OF_GAME_CONTROLS]; + byte reload_ticks; + long reload_down_time; +} keyboard_controls[MAXIMUM_GAMEPADS]; + +static void keyboard_hold_ticks( + byte *ticks, + long *down_time, + boolean down) +{ + long now = (long)system_milliseconds(); + + if (!down) + { + *ticks = 0; + } + else if (*ticks == 0) + { + *ticks = 1; + *down_time = now; + } + else + { + long held = now - *down_time; + + held = held < 0 ? 0 : MIN(held, (long)UNSIGNED_CHAR_MAX * 1000 / TICKS_PER_SECOND); + *ticks = (byte)PIN(1 + held * TICKS_PER_SECOND / 1000, 2, UNSIGNED_CHAR_MAX); + } + return; +} + +static void keyboard_controls_update( + long controller_index, + struct game_input_state *state) +{ + unsigned long held = halo_keyboard_actions((short)controller_index); + long control_index; + long x, y; + + for (control_index = 0; control_index < NUMBER_OF_GAME_CONTROLS; control_index++) + { + if (keyboard_game_controls[control_index] < 0) + continue; + keyboard_hold_ticks( + &keyboard_controls[controller_index].ticks[control_index], + &keyboard_controls[controller_index].down_times[control_index], + TEST_FLAG(held, keyboard_game_controls[control_index])); + state->buttons[control_index] = MAX(state->buttons[control_index], + keyboard_controls[controller_index].ticks[control_index]); + } + keyboard_hold_ticks( + &keyboard_controls[controller_index].reload_ticks, + &keyboard_controls[controller_index].reload_down_time, + TEST_FLAG(held, HALO_KEYBOARD_RELOAD)); + x = TEST_FLAG(held, HALO_KEYBOARD_STRAFE_RIGHT) - TEST_FLAG(held, HALO_KEYBOARD_STRAFE_LEFT); + y = TEST_FLAG(held, HALO_KEYBOARD_MOVE_FORWARD) - TEST_FLAG(held, HALO_KEYBOARD_MOVE_BACKWARD); + if (x || y) + { + /* (diagonals on the unit circle) */ + real length = x && y ? 0.70710678f : 1.f; + + state->forward_movement = y * length; + state->strafe = -x * length; + } + return; +} + +/* port: whether the controller's player holds the keyboard's reload key, and +for how many ticks its jump key (which skips cutscenes, as the +controller's A does) */ +boolean input_abstraction_port_reload( + short controller_index) +{ + return controller_index >= 0 && controller_index < MAXIMUM_GAMEPADS && + keyboard_controls[controller_index].reload_ticks != 0; +} + +boolean input_abstraction_port_action_only( + short controller_index) +{ + return controller_index >= 0 && controller_index < MAXIMUM_GAMEPADS && + keyboard_controls[controller_index].ticks[_game_control_action] != 0; +} + +byte input_abstraction_port_accept( + short controller_index) +{ + return controller_index >= 0 && controller_index < MAXIMUM_GAMEPADS ? + keyboard_controls[controller_index].ticks[_game_control_jump] : 0; +} + /* ---------- public code */ void input_abstraction_initialize( @@ -553,6 +668,7 @@ void input_abstraction_update( error(_error_silent, "unknown joystick preset"); break; } + keyboard_controls_update(controller_index, state); input_abstraction_globals.controller_available[controller_index] = TRUE; } else diff --git a/source/input/input_abstraction.h b/source/input/input_abstraction.h index 9bdfa0a4..34ee6fe6 100644 --- a/source/input/input_abstraction.h +++ b/source/input/input_abstraction.h @@ -53,6 +53,13 @@ void input_abstraction_update_local_player_preferences( struct game_input_preferences const *preferences); struct game_input_state *input_abstraction_get_input_state( short local_player_index); +/* port: the keyboard and mouse's own controls (input_abstraction.c) */ +boolean input_abstraction_port_reload( + short controller_index); +byte input_abstraction_port_accept( + short controller_index); +boolean input_abstraction_port_action_only( + short controller_index); /* ---------- globals */ diff --git a/source/interface/hud.c b/source/interface/hud.c index f0d43915..6e42f451 100644 --- a/source/interface/hud.c +++ b/source/interface/hud.c @@ -1002,6 +1002,7 @@ where they hide. */ /* the platform layer's (port/linux/src/port_config.c) */ const char *config_string(const char *name); double config_real(const char *name); +unsigned long config_changes(void); enum { @@ -1015,11 +1016,14 @@ static short hud_player_names_setting( void) { static short setting = NONE; + static unsigned long read_at = (unsigned long)-1; - if (setting == NONE) + /* (read again when Settings changes it) */ + if (read_at != config_changes()) { const char *value = config_string("display.player_names"); + read_at = config_changes(); setting = _player_names_all; if (value) { @@ -1041,9 +1045,13 @@ static real hud_player_name_scale( void) { static real scale = 0.0f; + static unsigned long read_at = (unsigned long)-1; - if (scale == 0.0f) + if (read_at != config_changes()) + { + read_at = config_changes(); scale = 0.75f * PIN((real)config_real("display.player_name_scale"), 0.25f, 4.0f); + } return scale; } diff --git a/source/interface/motion_sensor.c b/source/interface/motion_sensor.c index ce81dd12..9607f857 100644 --- a/source/interface/motion_sensor.c +++ b/source/interface/motion_sensor.c @@ -374,6 +374,9 @@ static void motion_sensor_update( local_player_index = local_player_get_next(local_player_index); } + /* (port: the scan shared by the local players: friends and enemies + alike, each player's own pass keeps his friends: update_motion_sensor) */ + game_engine_motion_sensor_viewer(NONE); object_iterator_new(&iter, _object_mask_unit, TRUE); while (object_iterator_next(&iter) && !done) { @@ -506,6 +509,8 @@ static void update_motion_sensor( csmemset(stack_buffer, 0x62, sizeof(stack_buffer)); player = get_motion_sensor_data(local_player_index); + /* (port: the FRIENDS radar's viewer: this player) */ + game_engine_motion_sensor_viewer(local_player_index); game_time_get(); if (motion_sensor_globals->update) { @@ -988,6 +993,7 @@ static void blip_begin( reference); blip_player_index = local_player_index; + game_engine_motion_sensor_viewer(local_player_index); scale[0] = in_multiplayer ? 0.75f : 1.0f; center_point = *reference; rasterizer_hud_motion_sensor_blip_begin(); diff --git a/source/interface/player_ui.c b/source/interface/player_ui.c index 644c6ad1..201f413d 100644 --- a/source/interface/player_ui.c +++ b/source/interface/player_ui.c @@ -225,6 +225,14 @@ static void clear_profile_edit_data( static long player1_last_used_profile_index = NONE; struct player_ui_globals player_ui_globals = { 0 }; +/* port: the PC options (game_engine.h) of the gametype being edited, as +edit_profile's variant, and of the multiplayer gametype */ +static struct +{ + struct game_variant_options original; + struct game_variant_options current; +} player_ui_edit_options; +static struct game_variant_options player_ui_multiplayer_options; static char player1_profile_path[0x100] = { 0 }; /* ---------- public code */ @@ -371,6 +379,13 @@ struct game_variant *player_ui_get_edit_playlist_profile( return result; } +/* port: the PC options of the gametype being edited (game_engine.h) */ +struct game_variant_options *player_ui_get_edit_playlist_options( + void) +{ + return player_ui_get_edit_playlist_profile() ? &player_ui_edit_options.current : NULL; +} + boolean player_ui_edit_profile_is_dirty( void) { @@ -412,7 +427,10 @@ boolean player_ui_edit_profile_is_dirty( if (csmemcmp( &player_ui_globals.edit_profile.original.variant, &player_ui_globals.edit_profile.current.variant, - sizeof(struct game_variant))) + sizeof(struct game_variant)) || + /* port: and its PC options */ + csmemcmp(&player_ui_edit_options.original, &player_ui_edit_options.current, + sizeof(struct game_variant_options))) { result = TRUE; } @@ -554,9 +572,24 @@ void player_ui_set_game_variant( csmemcpy(&player_ui_globals.multiplayer_variant, variant, sizeof(*variant)); player_ui_globals.multiplayer_variant_specified = TRUE; + /* port: its PC options, the defaults until they are given */ + game_variant_options_default(variant, &player_ui_multiplayer_options); return; } +/* port: the PC options of the multiplayer gametype (player_ui_set_game_variant's) */ +void player_ui_set_game_variant_options( + struct game_variant_options const *options) +{ + player_ui_multiplayer_options = *options; +} + +struct game_variant_options const *player_ui_get_game_variant_options( + void) +{ + return &player_ui_multiplayer_options; +} + boolean player_ui_game_variant_specified( struct game_variant *variant) { @@ -715,6 +748,9 @@ void player_ui_begin_editing_profile( &player_ui_globals.edit_profile.current.variant, &player_ui_globals.edit_profile.original.variant, sizeof(struct game_variant)); + /* port: and its PC options */ + playlist_profile_get_options(profile_index, &player_ui_edit_options.original); + player_ui_edit_options.current = player_ui_edit_options.original; } else { @@ -785,9 +821,10 @@ boolean player_ui_save_profile( player_ui_globals.edit_profile.current.variant.human_readable_game_description); if (new_profile_index != NONE) { - playlist_profile_save( + playlist_profile_save_with_options( new_profile_index, - &player_ui_globals.edit_profile.current.variant); + &player_ui_globals.edit_profile.current.variant, + &player_ui_edit_options.current); player_ui_globals.edit_profile_index = new_profile_index; if (saved_game_file_get_path_to_enclosing_directory( new_profile_index, @@ -812,9 +849,10 @@ boolean player_ui_save_profile( } else { - playlist_profile_save( + playlist_profile_save_with_options( player_ui_globals.edit_profile_index, - &player_ui_globals.edit_profile.current.variant); + &player_ui_globals.edit_profile.current.variant, + &player_ui_edit_options.current); if (saved_game_file_get_path_to_enclosing_directory( player_ui_globals.edit_profile_index, directory_path)) diff --git a/source/interface/player_ui.h b/source/interface/player_ui.h index dc87e76c..d38c900c 100644 --- a/source/interface/player_ui.h +++ b/source/interface/player_ui.h @@ -45,6 +45,15 @@ struct player_profile *player_ui_get_edit_player_profile( void); struct game_variant *player_ui_get_edit_playlist_profile( void); +/* port: the PC options (game_engine.h) of the gametype being edited, and of +the multiplayer gametype (player_ui_set_game_variant's) */ +struct game_variant_options; +struct game_variant_options *player_ui_get_edit_playlist_options( + void); +void player_ui_set_game_variant_options( + struct game_variant_options const *options); +struct game_variant_options const *player_ui_get_game_variant_options( + void); boolean player_ui_edit_profile_is_dirty( void); boolean player_ui_save_profile( diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 6c0ba20b..3cc2815f 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -682,7 +682,9 @@ struct widget_instance; enum { - WIDGET_MEMORY_POOL_SIZE = 0x4000, + /* port: 16 KB on the Xbox; the PC version's screens (port/assets/menus) + have many more widgets */ + WIDGET_MEMORY_POOL_SIZE = 0x40000, MAXIMUM_WIDGET_MEMORY_POOL_BLOCKS = 4096 }; @@ -1360,6 +1362,9 @@ static void event_handler_dispatch( static boolean ui_widget_load_children_recursive( struct widget_instance *widget, struct ui_widget_definition *definition); +/* port: whether the tag is one of the menus' (port/linux/game/menu_tags.c) */ +boolean pc_menu_tag( + long tag_index); static void widget_instance_initialize( struct widget_instance *widget, struct widget_instance *parent, @@ -2429,6 +2434,17 @@ static __inline boolean widget_instance_can_handle_events( return FALSE; } +/* port: a label in the PC version's lists (port/assets/menus): an item that +takes no events and has nothing in it that does, which its game passes over +(its rows of settings take none themselves, but their spinners do), or one +hidden (a list's rows past its items: port/linux/game/menu_functions.c) or +disabled (the server browser's column titles, which sort nothing) */ +static boolean widget_instance_port_is_label( + struct widget_instance *widget) +{ + return (!widget_instance_can_handle_events(widget) && !widget->child) || !widget->visible || widget->disabled; +} + static struct widget_instance *widget_instance_find_by_tag_index_recursive( struct widget_instance *widget, long tag_index) @@ -2628,6 +2644,25 @@ boolean widget_event_function_list_widget_goto_next_item( child = widget->child; item_index = 0; } + /* port: the PC version's lists (port/assets/menus) pass over the + children that take no events (their labels and lines), as its + game does */ + if (child && widget->type == _ui_widget_type_column_list && + pc_menu_tag(widget->definition_tag_index)) + { + long tries = 0; + + while (widget_instance_port_is_label(child) && tries++ < 256) + { + child = child->next; + item_index++; + if (!child) + { + child = widget->child; + item_index = 0; + } + } + } if (child) { widget_instance_give_focus_by_tag( @@ -2754,6 +2789,27 @@ boolean widget_event_function_list_widget_goto_previous_item( item_index++; } } + /* port: as goto_next_item, over the PC version's labels */ + if (widget->type == _ui_widget_type_column_list && pc_menu_tag(widget->definition_tag_index)) + { + long tries = 0; + + while (widget_instance_port_is_label(child) && tries++ < 256) + { + child = child->previous; + item_index--; + if (!child) + { + child = widget->child; + item_index = 0; + while (child->next) + { + child = child->next; + item_index++; + } + } + } + } widget_instance_give_focus_by_tag( widget, child->definition_tag_index, @@ -3554,17 +3610,27 @@ static boolean ui_widget_load_children_recursive( if (focus_a_child) { struct widget_instance *child; + /* port: the PC version's lists (port/assets/menus) start on their + first child that takes events, past their labels */ + boolean skip_labels = widget->type == _ui_widget_type_column_list && + pc_menu_tag(widget->definition_tag_index); + short index = 0; - for (child = widget->child; child; child = child->next) + for (child = widget->child; child; child = child->next, index++) { - if (widget->type == _ui_widget_type_spinner_list || - widget->type == _ui_widget_type_column_list || + if (((widget->type == _ui_widget_type_spinner_list || + widget->type == _ui_widget_type_column_list) && + (!skip_labels || !widget_instance_port_is_label(child))) || widget_instance_can_handle_events(child)) { widget->focused_child = child; + if (skip_labels) + widget->parameters.list.selected_index = index; break; } } + if (!widget->focused_child && skip_labels) + widget->focused_child = widget->child; } } @@ -3660,6 +3726,79 @@ static void widget_instance_initialize( return; } +/* port: the PC version's events that this engine never sends (its custom +activation), for the menus' functions (port/linux/game/menu_functions.c): +runs the widget's handlers for the event, else the first descendant's that +has any (depth first); FALSE if none has */ +boolean ui_widget_port_dispatch_event( + struct widget_instance *widget, + short event_type, + short controller_index, + boolean *deleted) +{ + struct ui_widget_definition *definition = ui_widget_definition_get(widget->definition_tag_index); + struct widget_instance *child; + boolean found = FALSE; + long handler_index; + + /* (deleted: a handler deleted the widget's screen, opening another or + going back; the callers' widgets are gone with it) */ + *deleted = FALSE; + + for (handler_index = 0; handler_index < definition->event_handlers.count; handler_index++) + { + struct ui_widget_event_handler_reference *handler = + (struct ui_widget_event_handler_reference *)definition->event_handlers.address + handler_index; + + if (handler->event_type == event_type) + { + struct event_record event = {0}; + boolean widget_deleted = FALSE; + + event.controller_index = controller_index; + event_handler_dispatch(widget, definition, &event, handler, &widget_deleted); + found = TRUE; + if (widget_deleted) + { + *deleted = TRUE; + return TRUE; + } + } + } + for (child = widget->child; child && !found; child = child->next) + { + found = ui_widget_port_dispatch_event(child, event_type, controller_index, deleted); + if (*deleted) + break; + } + return found; +} + +/* port: the number of a list's focused item: among all its children, but +in the PC version's lists (port/assets/menus) among those that take events, +past their labels, as its game counts them */ +short ui_widget_port_list_index( + struct widget_instance *list_widget) +{ + boolean skip_labels = pc_menu_tag(list_widget->definition_tag_index); + struct widget_instance *child; + short index = 0; + + for (child = list_widget->child; child && child != list_widget->focused_child; child = child->next) + { + if (!skip_labels || !widget_instance_port_is_label(child)) + index++; + } + return index; +} + +/* port: back to the screen before, for the menus' functions */ +void ui_widget_port_go_back( + struct widget_instance *widget) +{ + widget_instance_go_back_to_previous(widget); +} + struct widget_instance *ui_widget_load_by_name_or_tag( char const *name, long tag_index, @@ -4393,6 +4532,9 @@ void display_error_damaged_media( return; } +/* port: menu_tags.c's */ +char const *pc_menus_screen(char const *name); + void network_game_reset_to_pregame_ui( void) { @@ -4423,8 +4565,9 @@ void network_game_reset_to_pregame_ui( if (global_network_game_server_get()) { network_game_server_pause_countdown(global_network_game_server_get(), TRUE); + /* port: with the PC version's menus, theirs (port/linux/game/menu_tags.c) */ if (!ui_widget_load_by_name_or_tag( - "ui\\shell\\main_menu\\multiplayer_type_select\\connected\\connected_map_select_postgame_wrapper", + pc_menus_screen("ui\\shell\\main_menu\\multiplayer_type_select\\connected\\connected_map_select_postgame_wrapper"), NONE, NULL, NONE, NONE, NONE, NONE)) { error(_error_silent, "failed to load map select postgame screen"); @@ -4433,7 +4576,7 @@ void network_game_reset_to_pregame_ui( else { if (!ui_widget_load_by_name_or_tag( - "ui\\shell\\main_menu\\multiplayer_type_select\\connected\\pregame\\connected_pregame_screen", + pc_menus_screen("ui\\shell\\main_menu\\multiplayer_type_select\\connected\\pregame\\connected_pregame_screen"), NONE, NULL, NONE, NONE, NONE, NONE)) { error(_error_silent, "failed to load networked pregame status screen"); @@ -4684,8 +4827,13 @@ void main_screen_shell_load( { attract_mode_reset_timer(); ui_widgets_close_all(); - if (!ui_widget_load_by_name_or_tag("ui\\shell\\main_menu\\main_menu", NONE, NULL, NONE, NONE, NONE, NONE)) - error(_error_silent, "failed to load main screen shell window"); + /* port: the menus' main menu, when they are there (port/linux/game/menu_tags.c) */ + { + extern char const *pc_menus_root_name(void); + + if (!ui_widget_load_by_name_or_tag(pc_menus_root_name(), NONE, NULL, NONE, NONE, NONE, NONE)) + error(_error_silent, "failed to load main screen shell window"); + } if (widget_globals.main_menu_deferred_error_code != NONE) { display_error(widget_globals.main_menu_deferred_error_code, NONE, TRUE, FALSE); @@ -5628,11 +5776,22 @@ static boolean ui_mouse_menus_active( if (widget_globals.initialization_thread || progress_bar_is_active()) return FALSE; - return virtual_keyboard_active() || ui_mouse_menu() != NULL; + /* (and the scores after a game, which take A and B like the menus: + game_engine_update_non_deterministic) */ + return virtual_keyboard_active() || ui_mouse_menu() != NULL || game_engine_showing_postgame(); } /* the pointer's motion, clicks and wheel since the last frame, as the first player's controller events */ +/* port: a row of a list to choose from (the PC version's menus' lists of +gametypes, maps, profiles, levels and games: port/assets/menus) */ +static boolean ui_mouse_selection_row( + struct widget_instance *widget) +{ + return widget && widget->parent && pc_menu_tag(widget->parent->definition_tag_index) && + (!strncmp(widget->name, "list_item_", 10) || !strncmp(widget->name, "server_item_", 12)); +} + static void ui_widgets_process_mouse( void) { @@ -5684,7 +5843,10 @@ static void ui_widgets_process_mouse( { case _ui_mouse_target_item: case _ui_mouse_target_value: - ui_mouse_give_focus(target->widget); + /* (a selection list's row is chosen by a click, not + by passing over it on the way to its buttons) */ + if (!ui_mouse_selection_row(target->widget)) + ui_mouse_give_focus(target->widget); break; case _ui_mouse_target_list_slot: ui_mouse_step_list_to_slot(target->widget); @@ -5701,6 +5863,14 @@ static void ui_widgets_process_mouse( switch (target->kind) { case _ui_mouse_target_item: + /* (a selection list's row: chosen by a click, used by + a click on it chosen) */ + if (ui_mouse_selection_row(target->widget) && target->widget->parent && + target->widget->parent->focused_child != target->widget) + { + ui_mouse_give_focus(target->widget); + break; + } ui_mouse_give_focus(target->widget); ui_mouse_press(_gamepad_analog_button_a); break; @@ -6243,10 +6413,13 @@ static void widget_instance_tab_to_next_valid_widget( struct ui_widget_definition *definition = ui_widget_definition_get(child->definition_tag_index); - if (definition->event_handlers.count > 0 || + if ((definition->event_handlers.count > 0 || TEST_FLAG(definition->flags, _widget_pass_unhandled_events_to_children_bit) || widget->type == _ui_widget_type_spinner_list || - widget->type == _ui_widget_type_column_list) + widget->type == _ui_widget_type_column_list) && + /* port: over the PC version's labels and hidden rows */ + !(widget->type == _ui_widget_type_column_list && pc_menu_tag(widget->definition_tag_index) && + widget_instance_port_is_label(child))) { widget->focused_child = child; break; @@ -6282,10 +6455,13 @@ static void widget_instance_tab_to_previous_valid_widget( struct ui_widget_definition *definition = ui_widget_definition_get(child->definition_tag_index); - if (definition->event_handlers.count > 0 || + if ((definition->event_handlers.count > 0 || TEST_FLAG(definition->flags, _widget_pass_unhandled_events_to_children_bit) || widget->type == _ui_widget_type_spinner_list || - widget->type == _ui_widget_type_column_list) + widget->type == _ui_widget_type_column_list) && + /* port: over the PC version's labels and hidden rows */ + !(widget->type == _ui_widget_type_column_list && pc_menu_tag(widget->definition_tag_index) && + widget_instance_port_is_label(child))) { widget->focused_child = child; break; diff --git a/source/interface/ui_widget_event_handler_functions.c b/source/interface/ui_widget_event_handler_functions.c index 5f948a87..bfe802bd 100644 --- a/source/interface/ui_widget_event_handler_functions.c +++ b/source/interface/ui_widget_event_handler_functions.c @@ -928,6 +928,7 @@ symbols in this file: #include "interface/ui_widget_definitions.h" #include "saved games/saved_game_files.h" #include "text/unicode.h" +#include "halo_menus.h" /* port: PC_MENU_FUNCTION_BASE */ /* ---------- constants */ @@ -3350,6 +3351,14 @@ boolean ui_widget_event_handler_function_invoke( match_vassert("c:\\halo\\SOURCE\\interface\\ui_widget_event_handler_functions.c", 478, widget != NULL && widget_deleted != NULL, "(widget != NULL) && (widget_deleted != NULL)"); + /* port: the menus' own functions (port/linux/game/menu_functions.c) */ + if (function_index >= PC_MENU_FUNCTION_BASE && function_index < 0x8000) + { + extern boolean pc_menu_event_function_invoke(struct widget_instance *widget, struct event_record *event, + long function_index, boolean *widget_deleted); + + return pc_menu_event_function_invoke(widget, event, function_index - PC_MENU_FUNCTION_BASE, widget_deleted); + } if ((short)function_index >= 0 && function_index < 102) { result = event_handler_function_list.functions[(short)function_index](widget, event, widget_deleted); @@ -3361,6 +3370,49 @@ boolean ui_widget_event_handler_function_invoke( return FALSE; } +/* port: a function's name, as the tags name it, by its index (NULL past the +last), for the menus' files (port/linux/game/menu_tags.c) */ +char const *ui_widget_event_handler_function_name( + long function_index) +{ + return function_index >= 0 && function_index < NUMBEROF(event_handler_function_list.names) ? + event_handler_function_list.names[function_index] : NULL; +} + +/* port: the PC version's campaign menus (port/linux/game/menu_functions.c): +reads player 1's saved game, as the Xbox's level list does (which the +difficulty menu and its warning then use), and gives its map, its level and +its difficulty; FALSE if there is none */ +short main_get_solo_level_from_name(char const *name); + +boolean ui_widget_port_saved_game( + char const **map_name, + short *level, + short *difficulty) +{ + memset(&persistant_game_data_info, 0, sizeof(persistant_game_data_info)); + event_handler_functions.last_player1_profile_index = player_ui_get_active_player_profile_index(0); + if (event_handler_functions.last_player1_profile_index != NONE) + { + persistant_game_data_info.valid = game_state_test_persistent_storage( + persistant_game_data_info.map_name, + &persistant_game_data_info.difficulty.value, + &persistant_game_data_info.corrupted); + } + persistant_game_data_info.map_name[0xFF] = 0; + *level = main_get_solo_level_from_name(persistant_game_data_info.map_name); + if (persistant_game_data_info.valid != TRUE || *level == NONE) + { + persistant_game_data_info.valid = FALSE; + return FALSE; + } + persistant_game_data_info.map_index = (byte)*level; + persistant_game_data_info.difficulty.value = PIN(persistant_game_data_info.difficulty.value, 0, 3); + *map_name = persistant_game_data_info.map_name; + *difficulty = persistant_game_data_info.difficulty.value; + return TRUE; +} + static boolean new_campaign_chosen( struct widget_instance *widget, struct event_record *event, @@ -5772,3 +5824,288 @@ static boolean solo_level_initialize_list_single_player( } return TRUE; } + +/* port: the PC version's multiplayer menus (port/linux/game/menu_functions.c), +on our lists rather than the Xbox's spinners: */ + +/* the multiplayer maps (the Xbox's 13), and the one used last (else 0) */ +short ui_widget_port_multiplayer_maps( + char const *const **names, + short *last_used) +{ + char map_name[256]; + short level_index; + + *names = (char const *const *)event_handler_functions.multiplayer_levels; + *last_used = 0; + if (saved_game_file_retrieve_last_used_multiplayer_map(map_name)) + { + for (level_index = 0; level_index < 13; level_index++) + { + if (!_stricmp(map_name, event_handler_functions.multiplayer_levels[level_index])) + *last_used = level_index; + } + } + return 13; +} + +/* the map chosen (as multiplayer_level_select), the server's if there is +one; FALSE if this build cannot play it with others (said) */ +boolean ui_widget_port_multiplayer_map_choose( + short level_index) +{ + char const *map_name; + void *server = global_network_game_server_get(); + + if (level_index < 0 || level_index >= 13) + return FALSE; + map_name = event_handler_functions.multiplayer_levels[level_index]; + { + char build[0x20]; + + if (server && !network_game_is_splitscreen_local() && + !cache_files_map_plays_multiplayer(map_name, build)) + { + cache_files_show_multiplayer_unavailable(map_name, build); + return FALSE; + } + } + main_set_multiplayer_map_name(map_name); + game_engine_override_map_name(map_name); + if (server) + network_game_server_change_map_name(server, map_name); + saved_game_file_remember_last_used_multiplayer_map(event_handler_functions.multiplayer_levels[level_index]); + return TRUE; +} + +/* the gametypes (the built-in ones and those saved): their count, and the +one used last (else 0) */ +short ui_widget_port_gametypes( + long *indices, + short maximum, + short *last_used) +{ + char directory_path[256]; + word count = (word)maximum; + short index; + + playlist_profiles_enumerate_available_to_local_player_index(0, &count, indices); + *last_used = 0; + if (saved_game_file_retrieve_last_used_multiplayer_variant_directory(directory_path)) + { + long profile_index = saved_game_file_find_profile_index_for_directory_path(directory_path, 1); + + for (index = 0; profile_index != NONE && index < (short)count; index++) + { + if (indices[index] == profile_index) + *last_used = index; + } + } + return (short)count; +} + +/* the gametype chosen (as multiplayer_profile_set_for_game), the server's +if there is one */ +boolean ui_widget_port_gametype_choose( + long profile_index) +{ + struct game_variant profile; + char directory_path[256]; + void *server; + + if (profile_index == NONE || !(profile_index & 0x80000000)) + { + ui_play_audio_feedback_sound(4); + return FALSE; + } + if (!playlist_profile_get(profile_index, &profile)) + return FALSE; + server = global_network_game_server_get(); + if (saved_game_file_get_path_to_enclosing_directory(profile_index, directory_path)) + saved_game_file_remember_last_used_multiplayer_variant_directory(directory_path); + player_ui_set_game_variant(&profile); + /* (and its PC options: game_engine.h) */ + { + struct game_variant_options options; + + playlist_profile_get_options(profile_index, &options); + player_ui_set_game_variant_options(&options); + } + if (server) + network_game_server_change_game_variant(server, &profile); + return TRUE; +} + +/* hosting (as the Xbox's server list's Y) */ +boolean ui_widget_port_host( + struct widget_instance *widget, + struct event_record *event, + boolean *widget_deleted) +{ + return network_game_start_new_server(widget, event, widget_deleted); +} + +/* the game browsing (as the Xbox's server list): found games' client */ +boolean ui_widget_port_browse( + struct widget_instance *widget, + struct event_record *event, + boolean *widget_deleted) +{ + return global_network_game_client_get() || network_game_server_list_initialize(widget, event, widget_deleted); +} + +/* joining a found game (as network_game_join_game_from_server_list), then +the lobby (by name) in place of the widget's screen */ +boolean ui_widget_port_join( + struct widget_instance *widget, + void *advertised_game, + char const *lobby_name, + boolean *widget_deleted) +{ + byte *server = advertised_game; + struct transport_address address = { { { 0 } } }; + struct network_game_join_descriptor join_descriptor; + struct widget_instance *topmost_parent; + + if (!server || !global_network_game_client_get()) + return FALSE; + if (server[0xE0] != TRUE) + { + error(2, "attempted to join a closed game"); + ui_play_audio_feedback_sound(4); + return FALSE; + } + if (*(short *)(server + 0xDE) != 0 || + !network_game_client_advertised_game_compatible(global_network_game_client_get(), server, TRUE)) + { + return FALSE; + } + transport_client_start(server + 0x18, server + 8, server, 0x141E, &address); + if (!address.address.long_words[0] || !address.port) + { + error(2, "attempted to join a network game with a bogus address"); + return FALSE; + } + join_descriptor.unknown02 = 0; + network_game_generate_join_game_token(join_descriptor.token); + if (!network_game_client_initiate_join_game(global_network_game_client_get(), server, &join_descriptor, &address)) + { + network_game_abort(); + error(2, "failed to initiate join game procedures"); + return FALSE; + } + topmost_parent = widget_instance_get_topmost_parent(widget); + if (!ui_widget_load_by_name_or_tag(lobby_name, NONE, NULL, NONE, topmost_parent->definition_tag_index, + widget->parent ? widget->parent->definition_tag_index : NONE, widget_instance_get_child_index_from_parent(widget))) + { + error(2, "event handler failed to spawn widget"); + } + game_connection_set(1); + *widget_deleted = TRUE; + return TRUE; +} + +/* the player of the controller in a network game: player 1's profile (its +name one the host's ban command can name, as player_profile_set_for_game_1wide +asks), joining it (the lobby's "net splitscreen prejoin players" adds it) */ +boolean ui_widget_port_multiplayer_player( + short controller_index, + long profile_index) +{ + struct player_profile profile; + + if (controller_index < 0 || controller_index >= 4 || !player_profile_get(profile_index, &profile)) + return FALSE; + if (!player_name_valid(profile.player_name, NUMBEROF(profile.player_name))) + { + display_error_text_deferred( + L"Sorry, this profile's\r\nname can't be used in\r\nmultiplayer. Please\r\nrename the profile.", + controller_index); + ui_play_audio_feedback_sound(4); + return FALSE; + } + player_ui_set_active_player_profile(controller_index, profile_index, &profile); + player_ui_local_player_joined_multiplayer_game(controller_index); + return TRUE; +} + +/* a screen by name in place of the widget's (back returns to it: as +ui_widget_port_join opens the lobby) */ +boolean ui_widget_port_open( + struct widget_instance *widget, + char const *name, + boolean *widget_deleted) +{ + struct widget_instance *topmost_parent = widget_instance_get_topmost_parent(widget); + + if (!ui_widget_load_by_name_or_tag(name, NONE, NULL, NONE, topmost_parent->definition_tag_index, + widget->parent ? widget->parent->definition_tag_index : NONE, widget_instance_get_child_index_from_parent(widget))) + { + error(2, "event handler failed to spawn widget"); + return FALSE; + } + *widget_deleted = TRUE; + return TRUE; +} + +/* the gametype editor's: the gametype (a built-in one too: saving it asks +for a new name, player_ui_save_profile) being edited */ +boolean ui_widget_port_gametype_edit_begin( + long profile_index) +{ + if (profile_index == NONE || !(profile_index & 0x80000000)) + { + ui_play_audio_feedback_sound(4); + return FALSE; + } + player_ui_begin_editing_profile(profile_index); + return player_ui_get_edit_playlist_profile() != NULL; +} + +/* a saved gametype deleted (as delete_playlist_profile_final) */ +boolean ui_widget_port_gametype_delete( + long profile_index) +{ + if ((profile_index & 0xF) != 1 || (profile_index & 0x40000000)) + return FALSE; + playlist_profile_delete(profile_index); + return TRUE; +} + +/* the game's gametype (Server Setup's options' copy), the server's */ +boolean ui_widget_port_game_variant_set( + struct game_variant *variant, + struct game_variant_options const *options) +{ + void *server = global_network_game_server_get(); + + player_ui_set_game_variant(variant); + player_ui_set_game_variant_options(options); + if (server) + network_game_server_change_game_variant(server, variant); + return TRUE; +} + +/* the gametype editor's OK (as playlist_profile_save_changes, which fails +when nothing changed, after closing the screen: the menus show a failure) */ +boolean ui_widget_port_gametype_save( + struct widget_instance *widget, + boolean *widget_deleted) +{ + if (!player_ui_edit_profile_is_dirty()) + { + player_ui_end_editing_profile(); + ui_widget_delete(widget_instance_get_topmost_parent(widget)); + *widget_deleted = TRUE; + return TRUE; + } + /* (a built-in gametype changed: a new name asked first, the saving + screen not opened over it, as the Xbox's) */ + if (player_ui_edit_profile_is_default_profile() && !player_ui_edit_profile_name_is_dirty()) + { + player_ui_prompt_user_to_rename_edit_profile(); + return FALSE; + } + return player_ui_save_profile(); +} + diff --git a/source/interface/ui_widget_game_data_input_functions.c b/source/interface/ui_widget_game_data_input_functions.c index 0a0ba34b..0877ad6f 100644 --- a/source/interface/ui_widget_game_data_input_functions.c +++ b/source/interface/ui_widget_game_data_input_functions.c @@ -354,6 +354,7 @@ symbols in this file: #include "saved games/playlist_profile.h" #include "text/text_group.h" #include "text/unicode.h" +#include "halo_menus.h" /* port: PC_MENU_FUNCTION_BASE */ /* ---------- constants */ @@ -661,6 +662,12 @@ static void warn_if_difficulty_will_nuke_saved_game( static void dim_if_no_system_link_cable( struct widget_instance *widget); +/* port: ui_widget.c's and port/linux/game/menu_tags.c's */ +short ui_widget_port_list_index( + struct widget_instance *list_widget); +boolean pc_menu_tag( + long tag_index); + /* ---------- globals */ static ui_widget_game_data_function game_data_input_function_list[41]; @@ -690,6 +697,14 @@ void ui_widget_game_data_function_invoke( 0x10A, widget); + /* port: the menus' own functions (port/linux/game/menu_functions.c) */ + if (function >= PC_MENU_FUNCTION_BASE && function < 0x8000) + { + extern void pc_menu_game_data_function_invoke(struct widget_instance *widget, long function); + + pc_menu_game_data_function_invoke(widget, function - PC_MENU_FUNCTION_BASE); + return; + } if ((short)function >= 0 && function < NUMBEROF(game_data_input_function_list)) { game_data_input_function_list[(short)function](widget); @@ -730,15 +745,9 @@ static void settings_menu_update_extended_description( description_picture = list_widget->parameters.list.extended_description->child; description_text = description_picture->next; - child = list_widget->child; - index = 0; - while (child) - { - if (child == list_widget->focused_child) - break; - child = child->next; - index++; - } + /* port: in the PC version's lists (port/assets/menus), the item's number + among those that take events, as its game counts them (past its labels) */ + index = ui_widget_port_list_index(list_widget); if (index != NONE) { @@ -773,15 +782,9 @@ static void playlist_settings_menu_update_extended_description( list_widget && list_widget->focused_child && list_widget->parameters.list.extended_description, "invalid widget trying to update its extended list description"); - index = 0; - child = list_widget->child; - while (child) - { - if (child == list_widget->focused_child) - break; - child = child->next; - index++; - } + /* port: in the PC version's lists (port/assets/menus), the item's number + among those that take events, as its game counts them (past its labels) */ + index = ui_widget_port_list_index(list_widget); if (index != NONE) { @@ -814,15 +817,9 @@ static void playlist_gametype_select_menu_update_extended_description( list_widget && list_widget->focused_child && list_widget->parameters.list.extended_description, "invalid widget trying to update its extended list description"); - index = 0; - child = list_widget->child; - while (child) - { - if (child == list_widget->focused_child) - break; - child = child->next; - index++; - } + /* port: in the PC version's lists (port/assets/menus), the item's number + among those that take events, as its game counts them (past its labels) */ + index = ui_widget_port_list_index(list_widget); if (index != NONE) { @@ -856,15 +853,9 @@ static void multiplayer_type_menu_update_extended_description( list_widget->parameters.list.extended_description->child->next, "invalid widget trying to update its extended list description"); - index = 0; - child = list_widget->child; - while (child) - { - if (child == list_widget->focused_child) - break; - child = child->next; - index++; - } + /* port: in the PC version's lists (port/assets/menus), the item's number + among those that take events, as its game counts them (past its labels) */ + index = ui_widget_port_list_index(list_widget); if (index != NONE) { @@ -894,20 +885,19 @@ static void difficulty_select_menu_update_extended_description( match_vassert( "c:\\halo\\SOURCE\\interface\\ui_widget_game_data_input_functions.c", 0x238, - description_definition->child_count == 2, + /* port: the PC version's (port/assets/menus) has the profile's name too */ + description_definition->child_count == 2 || + (description_definition->child_count == 3 && pc_menu_tag(list_widget->definition_tag_index)), "this doesn't look like the difficulty select widget to me"); description_picture = list_widget->parameters.list.extended_description->child; description_text = description_picture->next; - child = list_widget->child; - index = 0; - while (child) - { - if (child == list_widget->focused_child) - break; - child = child->next; - index++; - } + /* port: in the PC version's lists, among the items that take events; its + buttons (past its four difficulties) leave the last one shown, which its + OK button then sets (port/linux/game/menu_functions.c) */ + index = ui_widget_port_list_index(list_widget); + if (pc_menu_tag(list_widget->definition_tag_index) && index >= 4) + index = NONE; if (index != NONE) { @@ -2243,6 +2233,10 @@ static void player_profile_edit_select_menu_update_extended_description( child = child->next; index++; } + /* port: the PC version's buttons (a list of their own, past its items) + leave the last item's description shown, as they have none */ + if (pc_menu_tag(list_widget->definition_tag_index) && list_widget->focused_child->type == _ui_widget_type_column_list) + index = NONE; if (index != NONE) { diff --git a/source/interface/virtual_keyboard.c b/source/interface/virtual_keyboard.c index ff041ed5..d12181f7 100644 --- a/source/interface/virtual_keyboard.c +++ b/source/interface/virtual_keyboard.c @@ -957,6 +957,13 @@ void virtual_keyboard_render( void virtual_keyboard_process( void) { + /* port: while it is up the computer's keyboard types into it + (port/linux/src/xinput_sdl.c) */ + { + extern void platform_text_typing(int typing); + + platform_text_typing(virtual_keyboard_globals.active); + } if (virtual_keyboard_globals.active) virtual_keyboard_process_internal(); @@ -1150,6 +1157,50 @@ static boolean virtual_keyboard_select( return TRUE; } +/* port: the characters and backspaces typed on the computer's keyboard, +as its keys would put them (the first replacing the text it began with) */ +static void virtual_keyboard_port_type( + void) +{ + struct key_stroke key; + + while (input_get_key(&key)) + { + boolean backspace = key.key_code == _key_backspace; + + if (!backspace && ((unsigned char)key.ascii_code < ' ' || (unsigned char)key.ascii_code > '~')) + continue; + if (virtual_keyboard_globals.first_key_replaces_buffer == TRUE) + { + csmemset(virtual_keyboard_globals.text_buffer, 0, virtual_keyboard_globals.buffer_size); + virtual_keyboard_globals.cursor = virtual_keyboard_globals.text_buffer; + virtual_keyboard_globals.first_key_replaces_buffer = FALSE; + if (backspace) + continue; + } + if (backspace) + { + virtual_keyboard_backspace(); + } + else if (virtual_keyboard_free_space_in_text_buffer() >= 2) + { + csmemmove( + virtual_keyboard_globals.cursor + 1, + virtual_keyboard_globals.cursor, + virtual_keyboard_globals.buffer_size - + ((byte *)virtual_keyboard_globals.cursor - (byte *)virtual_keyboard_globals.text_buffer) - + sizeof(wchar_t)); + *virtual_keyboard_globals.cursor++ = (wchar_t)(unsigned char)key.ascii_code; + ui_play_audio_feedback_sound(_ui_audio_feedback_forward); + } + else + { + ui_play_audio_feedback_sound(_ui_audio_feedback_flag_failure); + } + } + return; +} + static void virtual_keyboard_process_internal( void) { @@ -1159,6 +1210,8 @@ static void virtual_keyboard_process_internal( long action = NONE; boolean handled = FALSE; + virtual_keyboard_port_type(); + while (get_next_event(&event, NONE)) { switch (event.type) diff --git a/source/networking/network_client_manager.c b/source/networking/network_client_manager.c index a40d50f9..35cac6bb 100644 --- a/source/networking/network_client_manager.c +++ b/source/networking/network_client_manager.c @@ -3012,6 +3012,18 @@ boolean network_game_client_advertised_game_compatible( return FALSE; } +/* port: whether the advertised game is under way (HALO_PORT_ADVERTISED_IN_PROGRESS_FLAG), +not in its lobby */ +boolean network_game_client_advertised_game_in_progress( + struct network_game_client *client, + struct network_advertised_game const *game) +{ + long game_index = client ? game - client->available_games : NONE; + + return game_index >= 0 && game_index < MAXIMUM_NETWORK_ADVERTISED_GAMES && + (network_game_client_advertised_versions[game_index].flags & HALO_PORT_ADVERTISED_IN_PROGRESS_FLAG) != 0; +} + boolean network_game_client_join_first_available_game( void) { diff --git a/source/networking/network_client_manager.h b/source/networking/network_client_manager.h index 5d1fdc2a..3ef12727 100644 --- a/source/networking/network_client_manager.h +++ b/source/networking/network_client_manager.h @@ -41,6 +41,10 @@ boolean network_game_client_set_team( char team_index); /* whether the advertised game's host has this machine's network version (else the player is told, and it is not to be joined) */ +/* port: whether the advertised game is under way, not in its lobby */ +boolean network_game_client_advertised_game_in_progress( + struct network_game_client *client, + struct network_advertised_game const *game); boolean network_game_client_advertised_game_compatible( struct network_game_client *client, struct network_advertised_game const *game, diff --git a/source/networking/network_game_manager.c b/source/networking/network_game_manager.c index b687b143..c1dc844f 100644 --- a/source/networking/network_game_manager.c +++ b/source/networking/network_game_manager.c @@ -707,7 +707,11 @@ boolean network_game_create_game_objects( } if (game->variant.game_engine_index) + { game_set_game_variant(&game->variant); + /* port: and its PC options */ + game_set_game_variant_options(&game->variant_options); + } if (game_load(&options)) { diff --git a/source/networking/network_game_manager.h b/source/networking/network_game_manager.h index 6c846c53..b2619fce 100644 --- a/source/networking/network_game_manager.h +++ b/source/networking/network_game_manager.h @@ -62,6 +62,8 @@ struct network_game short __unknown426; unsigned long random_seed; long number_of_games_played; + /* port: the gametype's PC options (game_engine.h) */ + struct game_variant_options variant_options; struct network_game_local_data local_data; }; diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index 40791743..238e0631 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -472,6 +472,7 @@ symbols in this file: #include "text/unicode.h" #include "cache/cache_files.h" +#include "interface/player_ui.h" /* port: internet play's Discord presence (port/linux/src/p2p.c) */ void p2p_set_game_player_counts(int count, int maximum); @@ -752,6 +753,9 @@ static void network_game_server_dump( struct network_game_server *server); static void network_game_server_countdown_started( struct network_game_server *server); +static void network_game_server_variant_options( + struct game_variant const *variant, + struct game_variant_options *options); static void network_game_server_remove_players_gone_while_loading( struct network_game_server *server); @@ -1157,7 +1161,11 @@ struct network_game_server *network_game_server_create( error( _error_silent, "failed to create the server connection"); - network_game_server_dispose(server); + /* port: nothing to dispose of (the Xbox game disposed of it: its + client machines, all zero, have machine 0 with no connection, + which the dispose's machine check reads; the port in use, by + another copy of the game, gets here) */ + network_game_server_memory_do_not_use_directly_in_use = FALSE; server = NULL; } } @@ -1795,6 +1803,14 @@ boolean network_game_server_game_is_open( return game_is_open; } +/* port: whether the machines are loading the game (its start sent, still +in the pregame) */ +boolean network_game_server_game_is_loading( + struct network_game_server *server) +{ + return server->state == _network_game_server_state_pregame && server->sent_start_game_message; +} + boolean network_game_server_game_is_valid( struct network_game_server *server) { @@ -3102,6 +3118,7 @@ void network_game_server_change_game_variant( server->state == _network_game_server_state_pregame); csmemcpy(&server->game.variant, variant, sizeof(server->game.variant)); + network_game_server_variant_options(&server->game.variant, &server->game.variant_options); if (!network_game_server_send_game_data_pregame(server)) { @@ -3732,6 +3749,58 @@ static short network_game_server_get_client_machine_count( return client_machine_count; } +/* port: the PC menus' server settings (port/linux/game/menu_functions.c): +the game's name and the most players it takes, every game the server sets +up; none (empty, 0) keeps the Xbox's (the machine's name, every player the +native builds hold) */ +static struct +{ + wchar_t name[NETWORK_GAME_NAME_LENGTH]; + long maximum_players; +} network_game_server_port_settings; + +static void network_game_server_port_settings_apply( + struct network_game_server *server) +{ + if (network_game_server_port_settings.name[0]) + { + ustrncpy(server->game.name, network_game_server_port_settings.name, NETWORK_GAME_NAME_LENGTH - 1); + server->game.name[NETWORK_GAME_NAME_LENGTH - 1] = 0; + } + if (network_game_server_port_settings.maximum_players > 0) + { + server->game.maximum_players = (byte)PIN(network_game_server_port_settings.maximum_players, 2, + MAXIMUM_NETWORK_PLAYER_COUNT); + } +} + +void network_game_server_port_set_settings( + wchar_t const *name, + long maximum_players) +{ + struct network_game_server *server = global_network_game_server_get(); + + ustrncpy(network_game_server_port_settings.name, name ? name : L"", NETWORK_GAME_NAME_LENGTH - 1); + network_game_server_port_settings.name[NETWORK_GAME_NAME_LENGTH - 1] = 0; + network_game_server_port_settings.maximum_players = maximum_players; + if (server) + network_game_server_port_settings_apply(server); +} + +/* port: a gametype's PC options: the menus' (player_ui_set_game_variant_options) +when it is the menus' gametype, else its defaults */ +static void network_game_server_variant_options( + struct game_variant const *variant, + struct game_variant_options *options) +{ + struct game_variant chosen; + + if (player_ui_game_variant_specified(&chosen) && !csmemcmp(&chosen, variant, sizeof(chosen))) + *options = *player_ui_get_game_variant_options(); + else + game_variant_options_default(variant, options); +} + static boolean network_game_server_setup_game_from_playlist( struct network_game_server *server) { @@ -3750,6 +3819,8 @@ static boolean network_game_server_setup_game_from_playlist( server->game.map.version = 0; server->game.minimum_players = 2; server->game.maximum_players = MAXIMUM_NETWORK_PLAYER_COUNT; + network_game_server_port_settings_apply(server); + network_game_server_variant_options(&server->game.variant, &server->game.variant_options); if (server->game.variant.universal_variant.teams) { @@ -4167,6 +4238,35 @@ static boolean network_game_server_idle_pregame_tasks( { long itr; + /* port: the gametype's auto team balance (game_variant_options): + the lobby's teams kept within a player of each other, the bigger + team's last player moved */ + if (server->game.variant.universal_variant.teams && server->game.variant_options.auto_team_balance) + { + for (;;) + { + short count[2] = { 0, 0 }; + long last[2] = { NONE, NONE }; + short bigger; + + for (itr = 0; itr < MAXIMUM_NETWORK_PLAYER_COUNT; itr++) + { + struct network_player *player = &server->game.players[itr]; + + if (network_player_is_valid(player) && VALID_INDEX(player->team_index, 2)) + { + count[player->team_index]++; + last[player->team_index] = itr; + } + } + if (ABS(count[0] - count[1]) <= 1) + break; + bigger = count[0] > count[1] ? 0 : 1; + server->game.players[last[bigger]].team_index = 1 - bigger; + network_game_server_send_game_data_pregame(server); + } + } + /* send the lobby changes collected since the last settings update */ network_game_server_flush_game_data_pregame(server); diff --git a/source/networking/network_server_manager.h b/source/networking/network_server_manager.h index e23a4667..d7767b5d 100644 --- a/source/networking/network_server_manager.h +++ b/source/networking/network_server_manager.h @@ -49,6 +49,12 @@ enum { NETWORK_GAME_SERVER_NAME_TEXT_SIZE = 16, }; +/* port: the PC menus' server settings: the game's name (empty: the +machine's) and the most players (0: every player the build holds), for +every game the server sets up */ +void network_game_server_port_set_settings( + wchar_t const *name, + long maximum_players); boolean network_game_server_ban_player( char const *text); short network_game_server_matching_player_names( diff --git a/source/networking/network_server_manager_internal.h b/source/networking/network_server_manager_internal.h index 84ef3c69..8be0dee0 100644 --- a/source/networking/network_server_manager_internal.h +++ b/source/networking/network_server_manager_internal.h @@ -36,6 +36,9 @@ word network_game_server_get_state( short *substate); boolean network_game_server_game_is_open( struct network_game_server *server); +/* port: whether the machines are loading the game (its start sent) */ +boolean network_game_server_game_is_loading( + struct network_game_server *server); /* joining a distributed game in progress (network_server_manager.c) */ boolean network_game_server_accepts_late_joins( struct network_game_server *server); diff --git a/source/networking/network_server_message_handler.c b/source/networking/network_server_message_handler.c index 95a641ba..259a650a 100644 --- a/source/networking/network_server_message_handler.c +++ b/source/networking/network_server_message_handler.c @@ -1670,6 +1670,11 @@ static boolean network_game_server_handle_message_client_broadcast_game_search( advertisement.reserved[HALO_PORT_ADVERTISED_VERSION_OFFSET + 1] = (byte)(HALO_PORT_NETWORK_VERSION >> 8); advertisement.reserved[HALO_PORT_ADVERTISED_FLAGS_OFFSET] = HALO_PORT_ADVERTISED_DISTRIBUTED_FLAG; + if (network_game_server_get_state(server, NULL) != _network_game_server_state_pregame || + network_game_server_game_is_loading(server)) + { + advertisement.reserved[HALO_PORT_ADVERTISED_FLAGS_OFFSET] |= HALO_PORT_ADVERTISED_IN_PROGRESS_FLAG; + } /* (open to joins: not while the machines load the game, nor once it is over, nor in progress when no player can join it) */ if (network_game_server_get_state(server, NULL) == _network_game_server_state_ingame diff --git a/source/objects/damage.c b/source/objects/damage.c index d73a7e2d..5f00c31c 100644 --- a/source/objects/damage.c +++ b/source/objects/damage.c @@ -1381,6 +1381,8 @@ void object_cause_damage( short body_part; short object_number; long damaged_object_indices[16]; + /* port: the gametype's friendly fire (game_engine_friendly_damage) */ + short friendly_damage; /* the distributed netcode (port/linux/NETCODE.md): the host deals damage; a client reports its own players' hits instead, and the host @@ -1609,6 +1611,7 @@ void object_cause_damage( body_damage_multiplier = 0.f; being_damaged_flags = 0; body_part = NONE; + friendly_damage = _friendly_damage_all; if (collision_model_index != NONE) { @@ -1646,6 +1649,10 @@ void object_cause_damage( being_damaged_flags, _object_being_damaged_by_friendly_bit, TRUE); + friendly_damage = game_engine_friendly_damage(damage->owner_player_index, current_object_index, + TEST_FLAG(damage->flags, _damage_area_of_effect_bit) || + damage_definition->category == _damage_category_grenade || + damage_definition->category == _damage_category_highexplosive); } if (damaged_object_count == 0 && @@ -1701,7 +1708,8 @@ void object_cause_damage( TRUE); } - if (!TEST_FLAG(damage->flags, _damage_bypasses_shields_bit) && + if (friendly_damage != _friendly_damage_none && + !TEST_FLAG(damage->flags, _damage_bypasses_shields_bit) && !TEST_FLAG(damage_definition->flags, _damage_skips_shields_bit) && current_object->object.maximum_shield_vitality > 0.f && (damaged_object_count == 0 || @@ -1720,7 +1728,8 @@ void object_cause_damage( &total_damage); } - if ((damaged_object_count == 0 || + if (friendly_damage == _friendly_damage_all && + (damaged_object_count == 0 || (parent_takes_body_damage && TEST_FLAG( collision_model->resistance.flags, diff --git a/source/objects/object_types.c b/source/objects/object_types.c index a06813fb..d7febe32 100644 --- a/source/objects/object_types.c +++ b/source/objects/object_types.c @@ -1158,6 +1158,9 @@ void object_types_place_all( { short object_type; + /* port: the gametype's vehicles counted afresh */ + game_engine_vehicle_placement_begin(); + for (object_type = 0; object_type < NUMBER_OF_OBJECT_TYPES; object_type++) { struct object_type_definition *definition; @@ -1191,6 +1194,13 @@ void object_types_place_all( scenario_datum_index, element_size); + /* port: the gametype's vehicles of each team (game_variant_options: + every machine places the same) */ + if (object_type == _object_type_vehicle && + !game_engine_vehicle_placement_allowed(scenario_object, scenario_palette)) + { + continue; + } object_new_from_scenario(scenario_object, scenario_palette); objects_garbage_collection(); } diff --git a/source/saved games/playlist_profile.c b/source/saved games/playlist_profile.c index e0dec22e..d279483e 100644 --- a/source/saved games/playlist_profile.c +++ b/source/saved games/playlist_profile.c @@ -94,6 +94,13 @@ symbols in this file: enum { PLAYLIST_PROFILE_CHECKSUM_DATA_SIZE = 104, + /* port: the PC options (struct game_variant_options) after the + signature: a header (magic, version, size), the options, their own + signature. Builds before them write zeros (or the stack's bytes) here, + which do not pass. */ + PLAYLIST_PROFILE_OPTIONS_OFFSET = 0x100, + PLAYLIST_PROFILE_OPTIONS_MAGIC = 0x4F565047, /* 'GPVO' little-endian */ + PLAYLIST_PROFILE_OPTIONS_VERSION = 1, MAXIMUM_GAME_VARIANT_NAME_LENGTH = 12, NUMBER_OF_DEFAULT_PLAYLIST_PROFILES = 26, }; @@ -170,9 +177,22 @@ static unsigned long __stdcall playlist_profile_write_thread_proc( static void playlist_profile_write( long playlist_profile_index, struct game_variant *variant); +static boolean playlist_profile_read_block( + long playlist_profile_index, + byte *block); +static boolean playlist_profile_options_from_block( + byte const *block, + struct game_variant_options *options); +static void playlist_profile_options_to_block( + byte *block, + struct game_variant_options const *options); /* ---------- globals */ +/* port: the options the asynchronous write writes (playlist_profile_write), +the file's own when the saver gave none */ +static struct game_variant_options playlist_profile_write_options; + static struct playlist_profile_runtime_globals_prefix playlist_profile_globals = { 0 }; static struct playlist_profile_data playlist_profile_default_data = { @@ -452,6 +472,44 @@ boolean playlist_profile_get( void playlist_profile_save( long playlist_profile_index, struct game_variant *variant) +{ + struct game_variant_options options; + + /* port: the file's PC options kept */ + if (playlist_profile_index != NONE) + { + playlist_profile_get_options(playlist_profile_index, &options); + playlist_profile_save_with_options(playlist_profile_index, variant, &options); + } + + return; +} + +/* port: a gametype's PC options: the file's, else the defaults of its +variant (game_variant_options_default) */ +boolean playlist_profile_get_options( + long playlist_profile_index, + struct game_variant_options *options) +{ + byte block[SAVED_GAME_FILE_BLOCK_SIZE]; + + if (playlist_profile_index != NONE && + TEST_FLAG(playlist_profile_index, _saved_game_file_index_valid_bit) && + playlist_profile_read_block(playlist_profile_index, block)) + { + if (!playlist_profile_options_from_block(block, options)) + game_variant_options_default((struct game_variant *)block, options); + return TRUE; + } + game_variant_options_default(NULL, options); + return FALSE; +} + +/* port: a gametype saved with its PC options */ +void playlist_profile_save_with_options( + long playlist_profile_index, + struct game_variant *variant, + struct game_variant_options const *options) { match_assert( "c:\\halo\\SOURCE\\saved games\\playlist_profile.c", @@ -461,6 +519,17 @@ void playlist_profile_save( if (playlist_profile_index != NONE) { game_engine_variant_cleanup(variant); + /* (a write still running reads the options it was given: it ends + first) */ + if (playlist_profile_globals.thread) + { + while (!thread_has_exited(playlist_profile_globals.thread)) + { + } + dispose_thread(playlist_profile_globals.thread); + playlist_profile_globals.thread = NULL; + } + playlist_profile_write_options = *options; playlist_profile_write(playlist_profile_index, variant); } @@ -696,6 +765,9 @@ static unsigned long __stdcall playlist_profile_write_thread_proc( if (saved_game_file_open(&file, playlist_profile_index)) { + /* port: the block cleared (the Xbox game wrote its stack's bytes + after the signature), and the PC options after it */ + csmemset(block, 0, sizeof(block)); csmemcpy( block, variant, @@ -704,6 +776,7 @@ static unsigned long __stdcall playlist_profile_write_thread_proc( block, sizeof(struct game_variant), (struct _XCALCSIG_SIGNATURE *)(block + sizeof(struct game_variant))); + playlist_profile_options_to_block(block, &playlist_profile_write_options); if (!file_set_position(&file, 0) || !file_write(&file, sizeof(block), block)) @@ -788,3 +861,82 @@ static void playlist_profile_write( return; } + +/* port: a gametype file's block, read whole */ +static boolean playlist_profile_read_block( + long playlist_profile_index, + byte *block) +{ + struct file_reference file; + boolean success = FALSE; + + if (playlist_profile_globals.thread) + { + while (!thread_has_exited(playlist_profile_globals.thread)) + { + } + dispose_thread(playlist_profile_globals.thread); + playlist_profile_globals.thread = NULL; + } + if (saved_game_files_take_mutex()) + { + if (saved_game_file_open(&file, playlist_profile_index)) + { + success = file_read(&file, SAVED_GAME_FILE_BLOCK_SIZE, block); + saved_game_file_close(&file, playlist_profile_index); + } + saved_game_files_release_mutex(); + } + return success; +} + +struct playlist_profile_options_header +{ + unsigned long magic; + word version; + word size; +}; + +static boolean playlist_profile_options_from_block( + byte const *block, + struct game_variant_options *options) +{ + struct playlist_profile_options_header header; + byte const *data = block + PLAYLIST_PROFILE_OPTIONS_OFFSET; + unsigned long size = sizeof(header) + sizeof(*options); + XCALCSIG_SIGNATURE checksum; + + csmemcpy(&header, data, sizeof(header)); + if (header.magic != PLAYLIST_PROFILE_OPTIONS_MAGIC || header.version != PLAYLIST_PROFILE_OPTIONS_VERSION || + header.size != sizeof(*options)) + { + return FALSE; + } + saved_game_file_generate_checksum((byte *)data, size, &checksum); + if (csmemcmp(&checksum, data + size, sizeof(checksum))) + return FALSE; + csmemcpy(options, data + sizeof(header), sizeof(*options)); + options->friendly_fire = (short)PIN(options->friendly_fire, 0, NUMBER_OF_FRIENDLY_FIRE_MODES - 1); + options->radar_players = (byte)PIN(options->radar_players, 0, NUMBER_OF_RADAR_PLAYERS - 1); + return TRUE; +} + +static void playlist_profile_options_to_block( + byte *block, + struct game_variant_options const *options) +{ + struct playlist_profile_options_header header; + byte *data = block + PLAYLIST_PROFILE_OPTIONS_OFFSET; + unsigned long size = sizeof(header) + sizeof(*options); + + header.magic = PLAYLIST_PROFILE_OPTIONS_MAGIC; + header.version = PLAYLIST_PROFILE_OPTIONS_VERSION; + header.size = sizeof(*options); + csmemcpy(data, &header, sizeof(header)); + csmemcpy(data + sizeof(header), options, sizeof(*options)); + saved_game_file_generate_checksum(data, size, (XCALCSIG_SIGNATURE *)(data + size)); +} + +typedef char verify_playlist_profile_options_fit[ + PLAYLIST_PROFILE_OPTIONS_OFFSET + sizeof(struct playlist_profile_options_header) + + sizeof(struct game_variant_options) + sizeof(XCALCSIG_SIGNATURE) <= SAVED_GAME_FILE_BLOCK_SIZE ? 1 : -1]; diff --git a/source/saved games/playlist_profile.h b/source/saved games/playlist_profile.h index af203f7d..5438c7f5 100644 --- a/source/saved games/playlist_profile.h +++ b/source/saved games/playlist_profile.h @@ -37,6 +37,16 @@ long playlist_profile_new( void playlist_profile_save( long playlist_profile_index, struct game_variant *variant); +/* port: a gametype's PC options (game_engine.h), and a gametype saved with +them */ +struct game_variant_options; +boolean playlist_profile_get_options( + long playlist_profile_index, + struct game_variant_options *options); +void playlist_profile_save_with_options( + long playlist_profile_index, + struct game_variant *variant, + struct game_variant_options const *options); void playlist_profile_delete( long playlist_profile_index); boolean playlist_profile_get_from_path( diff --git a/source/sound/sound_manager.c b/source/sound/sound_manager.c index b3d3932a..0a8e573e 100644 --- a/source/sound/sound_manager.c +++ b/source/sound/sound_manager.c @@ -1115,10 +1115,31 @@ static short sound_definition_promote( return result; } +/* port: config.toml's audio.music_volume and audio.effects_volume, read +again when Settings changes them */ +double config_real(const char *name); +unsigned long config_changes(void); + +static real sound_manager_port_volume( + short class_index) +{ + static unsigned long read_at = (unsigned long)-1; + static real music_volume = 1.f; + static real effects_volume = 1.f; + + if (read_at != config_changes()) + { + read_at = config_changes(); + music_volume = PIN((real)config_real("audio.music_volume"), 0.f, 1.f); + effects_volume = PIN((real)config_real("audio.effects_volume"), 0.f, 1.f); + } + return class_index == _sound_class_music ? music_volume : effects_volume; +} + static real sound_manager_master_gain( short class_index) { - real gain = sound_class_get_gain(class_index); + real gain = sound_class_get_gain(class_index) * sound_manager_port_volume(class_index); if (class_index != _sound_class_scripted_dialog_to_player && class_index != _sound_class_scripted_dialog_to_other && diff --git a/source/units/bipeds.c b/source/units/bipeds.c index 11960a70..47788aac 100644 --- a/source/units/bipeds.c +++ b/source/units/bipeds.c @@ -276,6 +276,10 @@ symbols in this file: #include "scenario/scenario.h" #include "structures/structure_bsp_definitions.h" +/* port: an unarmed player's melee's length, in ticks (a weapon's is about +this: its first person melee animation, sped up a quarter) */ +#define UNARMED_MELEE_TICKS 16 + /* ---------- constants */ enum @@ -4286,12 +4290,25 @@ boolean biped_update( biped_index, unit_get(biped_index)->unit.current_weapon_index); - if (!weapon_prevents_melee_attack(weapon_index) && + /* (port: and with no weapon, which prevents it in the + Xbox game: a gametype's loadout of none) */ + if ((weapon_index == NONE || !weapon_prevents_melee_attack(weapon_index)) && biped->unit.current_zoom_level==NONE) { short melee_speedup_ticks; unit_animation_start_action(biped_index, _unit_animation_action_melee); + /* port: a player with no weapon (a gametype's loadout of + none) melees too: in a weapon's usual time, the hit + halfway (the Xbox game read the timing from the weapon's + animations, through a weapon it did not check) */ + if (weapon_index == NONE) + { + biped->biped.player_melee_ticks = UNARMED_MELEE_TICKS; + biped->biped.player_melee_attack_tick = UNARMED_MELEE_TICKS / 2; + } + else + { weapon_stop_reload(weapon_index); first_person_weapon_message_from_unit( biped_index, @@ -4307,6 +4324,7 @@ boolean biped_update( _weapon_first_person_animation_time_private_key_frame, _first_person_weapon_animation_melee, NONE); + } melee_speedup_ticks = biped->biped.player_melee_ticks >> 2; biped->biped.player_melee_ticks -= melee_speedup_ticks; diff --git a/source/units/units.h b/source/units/units.h index 20ca7cca..6c78ee83 100644 --- a/source/units/units.h +++ b/source/units/units.h @@ -124,6 +124,15 @@ enum FLAG(_unit_control_swap_weapons_bit), }; +/* port: the keyboard's action key (port/linux/include/halo_keyboard.h), with +the action: the action alone, not the reload the controller's X falls back +to when there is nothing to act on (players.c). It goes with the player's +action (and over the network) but never to the unit. */ +#define UNIT_CONTROL_PORT_ACTION_ONLY_BIT 15 +typedef char verify_unit_control_port_action_only_bit[ + UNIT_CONTROL_PORT_ACTION_ONLY_BIT >= NUMBER_OF_UNIT_CONTROL_FLAGS && + UNIT_CONTROL_PORT_ACTION_ONLY_BIT < 16 ? 1 : -1]; + enum { _unit_state_idle = 0, diff --git a/tools/android_build.py b/tools/android_build.py index 48802e10..581bff32 100644 --- a/tools/android_build.py +++ b/tools/android_build.py @@ -38,6 +38,8 @@ BUILD = Path("build/android") THIRD_PARTY = BUILD / "third_party" # the TOML parser config.toml is read with (port/linux/src/port_config.c) TOML_DIR = Path("port/third_party/tomlc17") +EXPAT_DIR = Path("port/third_party/expat") +EXPAT_SOURCES = ("xmlparse.c", "xmlrole.c", "xmltok.c") KCP_DIR = Path("port/third_party/kcp") MUSL_VERSION = "1.2.5" MUSL_DIR = THIRD_PARTY / f"musl-{MUSL_VERSION}" @@ -401,7 +403,7 @@ def generate_android_build(n: Writer, sln: Any) -> None: guest_abi, guest_code, "-std=gnu11", "-D_GNU_SOURCE", "-DHALO_LINUX_PLATFORM_LAYER", "-w", profile_flags, f"-include {prefix_header}", f"-include {platform_semantics_header}", f"-I{LINUX_DIR}/src", f"-I{LINUX_DIR}/include", f"-I{PORT_DIR}/guest/runtime", - f"-I{PORT_DIR}/include", f"-I{TOML_DIR}", f"-I{KCP_DIR}", "-Isource -Isource/cseries", + f"-I{PORT_DIR}/include", f"-I{TOML_DIR}", f"-I{EXPAT_DIR}", f"-I{KCP_DIR}", "-Isource -Isource/cseries", f"-I{SDL_DIR}/include", f"-I{gl_include}", *libc_includes, f"-idirafter {XDK_INCLUDE}", ]) guest_host_only = {"memory_watch.c"} # replaced by guest_memory_watch.c @@ -414,6 +416,9 @@ def generate_android_build(n: Writer, sln: Any) -> None: objects.append(guest_object(source, platform_cflags)) # the settings file's parser (port/third_party/tomlc17) objects.append(guest_object(TOML_DIR / "tomlc17.c", platform_cflags)) + # the menus' XML parser (port/third_party/expat; menu_files.c) + for name in EXPAT_SOURCES: + objects.append(guest_object(EXPAT_DIR / name, platform_cflags)) # internet play's reliable streams (port/third_party/kcp; p2p.c) objects.append(guest_object(KCP_DIR / "ikcp.c", platform_cflags)) # the game's sin, pow and the rest, the same on every port diff --git a/tools/ce_menus.py b/tools/ce_menus.py new file mode 100644 index 00000000..6bc2c2b0 --- /dev/null +++ b/tools/ce_menus.py @@ -0,0 +1,670 @@ +#!/usr/bin/env python3 +"""Writes the PC version's menus (Halo Custom Edition's) as the port's menu +files (port/assets/menus/ce, read by port/linux/src/menu_files.c): + + python tools/ce_menus.py --tags .../halopc-restored/tags \\ + --definitions .../invader/src/tag/hek/definition \\ + --redraws .../ui-svg-handmade --placeholder .../placeholder.png \\ + [--pictures .../extracted-ui-assets-fixed] + +Every widget reachable from the PC main menu (ui\\shell\\main_menu\\main_menu: +its children, the widgets its handlers open, their conditional widgets and +list descriptions, and so on) is written as a , laid out as the PC +version has it, with its string lists as and its bitmaps as +s. Our widgets are named by their tag's path below ui\\shell, with +forward slashes (main_menu/main_menu); the root is the main menu. + +The bitmaps are drawn from the high-res redraws (--redraws, hand-made SVGs +in the PC bitmaps' layout), copied to port/assets/menus/svg, at up to 4 +times their size. The pictures with no redraw (renders, screenshots, logos) +are the Xbox map's bitmap of the same name, where it has the same pictures +in the same order (XBOX_BITMAPS: the player's own ui.map draws them, and +nothing is shipped), else the --placeholder image, fitted to each frame +where the PC version's picture is (--pictures: only their bounds are read), +else to the part of it the widgets show; +port/assets/menus/NON_HANDDRAWN.md lists both, the placeholders as ones to +be redrawn. + +What this engine (the Xbox's) has no use for is written but never runs: the +PC version's mouse events (the port's own mouse support clicks with A), and +its own functions (port/linux/game/menu_functions.c, most of which do +nothing yet). Its gamespy and ticker fonts are drawn with small_ui, which +the Xbox's map has. + +Needs rsvg-convert, Pillow, NumPy and SciPy; port/assets/menus/menus.json, +which tools/embed_assets.py embeds the files from, is written too. +""" + +import argparse +import json +import shutil +import struct +import sys +from collections import Counter, defaultdict +from pathlib import Path +from typing import Optional +from xml.sax.saxutils import quoteattr + +from PIL import Image + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from hud_assets import bleed, render_svg # noqa: E402 +import port_settings # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent +MENUS = ROOT / "port/assets/menus" +CE = MENUS / "ce" +SVG_COPIES = MENUS / "svg" +MAIN_MENU = "ui\\shell\\main_menu\\main_menu" +TITLE_FONT = ROOT / "port/assets/fonts/OpenCE-Regular.ttf" +# the widgets left out, with their pictures (and all they alone lead to): +# the server browser's GameSpy logo, which the port has no use for, and the +# main menu's Credits, which the Xbox's game has not +LEFT_OUT = { + "ui\\shell\\main_menu\\multiplayer_type_select\\join_game\\header_gamespy_logo", + "ui\\shell\\main_menu\\main_menu_item_credits", + # (the "checking for updates" dialogs, which asked the PC version's + # servers: Create > Internet and Join > Server Browser skip them, + # port_settings.WIDGET_PATCHES) + "ui\\shell\\main_menu\\multiplayer_type_select\\checking_updates_screen_create", + "ui\\shell\\main_menu\\multiplayer_type_select\\checking_updates_screen_join", +} +# children moved: (parent, child) to (x, y); the main menu's Quit up into +# Credits' place +CHILD_OFFSETS = { + ("ui\\shell\\main_menu\\main_menu_select_list", "ui\\shell\\main_menu\\main_menu_item_quit_game"): (192, 391), + # the browser's rows up under the column titles, into the place of the + # scroll up button (hidden: the list does not scroll) + **{("ui\\shell\\main_menu\\multiplayer_type_select\\join_game\\join_game_items_list", + f"ui\\shell\\main_menu\\multiplayer_type_select\\join_game\\server_item_{row}"): (10, 102 + 17 * (row - 1)) + for row in range(1, 16)}, +} +SCALE = 4 +MAXIMUM_SIZE = 2048 +# the PC fonts this engine's map lacks, and the font drawn instead +FONTS = {"ui\\gamespy": "ui\\small_ui", "ui\\ticker": "ui\\small_ui"} + +# the XML's names of the tags' values (menu_tags.c has the same) +EVENTS = ["a", "b", "x", "y", "black", "white", "left_trigger", "right_trigger", "up", "down", "left", "right", + "start", "back", "left_thumb", "right_thumb", "stick_up", "stick_down", "stick_left", "stick_right", + "right_stick_up", "right_stick_down", "right_stick_left", "right_stick_right", "created", "deleted", + "get_focus", "lose_focus", "left_mouse", "middle_mouse", "right_mouse", "double_click", + "custom_activation", "post_render"] +WIDGET_FLAGS = ["pass_unhandled_to_focused_child", "pause_game", "flash_bitmap", "up_down_tabs_children", + "left_right_tabs_children", "up_down_tabs_items", "left_right_tabs_items", "no_focused_child", + "pass_unhandled_to_all_children", "render_any_controller", "pass_handled_to_all_children", + "main_menu_if_no_history", "tag_controller_index", "nifty_fx", "no_history", "force_handle_mouse", + "no_widescreen_fill"] +TEXT_FLAGS = ["editable", "password", "flashing", "no_focus_test"] +LIST_FLAGS = ["items_in_code", "items_from_strings", "one_tooltip", "single_preview"] +TYPES = ["container", "text", "spinner", "column_list"] +ALIGN = ["left", "right", "center"] +REPLACE = [None, "controller", "build_number", "pid"] + +# the PC bitmaps with no redraw whose pictures the Xbox's ui.map has, under +# the same name and in the same order: drawn from the player's map. (The +# Xbox's mp_map_grafix has the 13 Xbox maps' pictures, as the PC version's +# first 13, and its "?" after them; the PC version's other 8 are its own +# maps', which the Xbox's game has not.) +XBOX_BITMAPS = { + "ui\\shell\\bitmaps\\colors_sm", "ui\\shell\\bitmaps\\mp_map_grafix", "ui\\shell\\bitmaps\\sp_levels", + "ui\\shell\\main_menu\\difficulty_select\\difficulty_options", + "ui\\shell\\main_menu\\difficulty_select\\difficulty_options_small", "ui\\shell\\main_menu\\halo_logo", + "ui\\shell\\main_menu\\settings_select\\player_setup\\player_profile_edit\\color_edit\\player_color_marine_large", +} +# the frames with no redraw that are drawn from a frame of the Xbox map's +# bitmap of the same name, whose other frames differ (): +# the same picture, or for the profile settings the Xbox's for the same +# thing (Change Name; its Controller Setup for Gamepads) +XBOX_FRAMES = { + "shell/main_menu/multiplayer_type_select/mp_options__2": 3, + "shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__2": 2, + "shell/main_menu/settings_select/multiplayer_setup/playlist_edit/gametype_options__5": 4, + "shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__0": 0, + "shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__2": 1, + "shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__7": 3, +} +PLACEHOLDER_SCALE = 2 + +# the Xbox's functions that the PC version rewrote for its own widgets (its +# column lists where the Xbox had spinners, and other children), which stop +# this engine on the PC widgets: written as "unwired ", which does +# nothing (UNWIRED.md) +INCOMPATIBLE = set() +# the PC version's own functions that menu_functions.c does what they do +# (and its campaign's: the Xbox's of those names, which take the Xbox's +# widgets, menu_functions.c's for ours) +WIRED = { + "main menu quit game", "profile set edit begin", "mouse emit accept event", "mouse emit back event", + "mouse emit x event", "emit custom activation event", "single prev cl item activated", "controls back handler", + "gamespy back handler", "gamespy dismiss error", "gamespy dismiss filters", "gamespy screen init", "mp type set mode", + "campaign menu init", "campaign menu continue", "difficulty item select", "solo map list update", + "load game menu init", "load game menu dispose", "load game menu activated", "load game list update", + "load game menu delete request", "load game menu delete finish", + "controls screen init", "controls begin binding", "controls screen change set", "controls screen defaults", + "controls update menu", "profile manager select", "direct ip connect go", +} + +# ---------- HEK tags + + +class Tags: + """HEK tags (big-endian), read with invader's definitions""" + + BASIC = {"int8": 1, "uint8": 1, "int16": 2, "uint16": 2, "int32": 4, "uint32": 4, "float": 4, "Fraction": 4, + "Angle": 4, "TagFourCC": 4, "TagID": 4, "Pointer": 4, "Index": 2, "Point2D": 8, "Point2DInt": 4, + "Rectangle2D": 8, "ColorARGB": 16, "ColorRGB": 12, "TagString": 32, "TagDependency": 16, + "TagReflexive": 12, "TagDataOffset": 20} + STRUCTS = {"ui_widget_definition": "UIWidgetDefinition", "bitmap": "Bitmap", + "unicode_string_list": "UnicodeStringList"} + + def __init__(self, tags: Path, definitions: Path): + self.root = tags + self.types = {} + for path in sorted(definitions.glob("*.json")): + for item in json.loads(path.read_text()): + self.types.setdefault(item["name"], item) + + def size(self, field: dict) -> int: + kind = field["type"] + if kind == "pad": + return field["size"] + if kind in self.BASIC: + return self.BASIC[kind] * field.get("count", 1) + definition = self.types[kind] + if definition["type"] == "enum": + return 2 + if definition["type"] == "bitfield": + return definition["width"] // 8 + return definition["size"] * field.get("count", 1) + + def options(self, name: str) -> list: + return [o if isinstance(o, str) else o["name"] for o in self.types[name]["options"]] + + def parse(self, name: str, data: bytes, base: int, cursor: list) -> dict: + """the struct at base; what it points to follows from cursor[0], in + the order of its fields""" + result, position, later = {}, base, [] + for field in self.types[name]["fields"]: + kind, key, size = field["type"], field.get("name"), self.size(field) + raw = data[position:position + size] + position += size + if kind == "pad" or not key: + continue + if kind == "TagString": + result[key] = raw.split(b"\0")[0].decode("latin-1") + elif kind == "TagDependency": + _, _, length, _ = struct.unpack(">4sIiI", raw) + result[key] = None + later.append(("dependency", key, length, None)) + elif kind == "TagReflexive": + count = struct.unpack(">i", raw[:4])[0] + result[key] = [] + later.append(("block", key, count, field["struct"])) + elif kind == "TagDataOffset": + result[key] = b"" + later.append(("data", key, struct.unpack(">i", raw[:4])[0], None)) + elif kind == "Rectangle2D": + result[key] = struct.unpack(">4h", raw) + elif kind == "ColorARGB": + result[key] = struct.unpack(">4f", raw) + elif kind in ("float", "Fraction", "Angle"): + result[key] = struct.unpack(">f", raw)[0] + elif size == 2: + unsigned = kind == "uint16" or self.types.get(kind, {}).get("type") == "bitfield" + result[key] = struct.unpack(">H" if unsigned else ">h", raw)[0] + elif size == 4: + unsigned = kind == "uint32" or self.types.get(kind, {}).get("type") == "bitfield" + result[key] = struct.unpack(">I" if unsigned else ">i", raw)[0] + else: + result[key] = raw + for kind, key, count, element in later: + if kind == "dependency": + if count > 0: + result[key] = data[cursor[0]:cursor[0] + count].decode("latin-1") + cursor[0] += count + 1 + elif kind == "data": + result[key] = data[cursor[0]:cursor[0] + count] + cursor[0] += count + else: + element_size = self.types[element]["size"] + start = cursor[0] + cursor[0] += element_size * count + result[key] = [self.parse(element, data, start + index * element_size, cursor) + for index in range(count)] + return result + + def path(self, tag: str, group: str) -> Path: + return self.root / (tag.replace("\\", "/") + "." + group) + + def load(self, tag: str, group: str) -> dict: + data = self.path(tag, group).read_bytes() + name = self.STRUCTS[group] + return self.parse(name, data, 64, [64 + self.types[name]["size"]]) + + +# ---------- names + + +def our_name(tag: str) -> str: + """a tag's name in our files: its path below ui\\shell, with slashes""" + for prefix in ("ui\\shell\\", "ui\\"): + if tag.startswith(prefix): + return tag[len(prefix):].replace("\\", "/") + return tag.replace("\\", "/") + + +def attributes(pairs: list) -> str: + return "".join(f" {key}={quoteattr(str(value))}" for key, value in pairs if value is not None) + + +def text_value(utf16: bytes) -> str: + text = utf16.decode("utf-16-le").split("\0")[0] + return text.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "") + + +def flag_names(value: int, names: list) -> str: + return " ".join(name for bit, name in enumerate(names) if value >> bit & 1) or None + + +def bounds_text(bounds: tuple) -> str: + return " ".join(str(value) for value in bounds) + + +# ---------- the art + + +class Art: + def __init__(self, tags: Tags, redraws: Path, placeholder: Path, shown: dict, pictures: Optional[Path]): + self.tags, self.redraws, self.shown, self.pictures_folder = tags, redraws, shown, pictures + self.placeholder = Image.open(placeholder).convert("RGBA") + self.pictures = [] # (frame, size, PC picture) of the frames drawn as the placeholder + self.xbox_frames = [] # (PC frame, Xbox bitmap, its frame) drawn from the Xbox map + self.svgs = [] + self.pngs = [] + + def title(self, name: str, text: str) -> str: + """a of a title this port has that the PC version has not: + the text set in OpenCE (the public-domain Newtown respaced, as the + port's titles are: tools/title_assets.py) over the PC headers' + backdrop""" + from PIL import ImageDraw, ImageFont + scale = SCALE + probe = ImageFont.truetype(str(TITLE_FONT), 1000) + left, top, right, bottom = probe.getbbox("H") + font = ImageFont.truetype(str(TITLE_FONT), round(port_settings.TITLE_CAP * scale * 1000 / (bottom - top))) + width = font.getlength(text) / scale + svg = MENUS / "port_svg" / f"{name}.svg" + svg.parent.mkdir(parents=True, exist_ok=True) + svg.write_text(port_settings.title_backdrop(width)) + image = Image.fromarray(render_svg(svg, scale), "RGBA") + draw = ImageDraw.Draw(image) + cap_top = font.getbbox("H")[1] + draw.text((port_settings.TITLE_LEFT * scale, port_settings.TITLE_TOP * scale - cap_top), text, + font=font, fill=port_settings.TITLE_COLOR) + png = f"ce/port/{name}.png" + (MENUS / png).parent.mkdir(parents=True, exist_ok=True) + image.save(MENUS / png, optimize=True) + self.pngs.append(png) + return "\n".join([f"\t", + f"\t\t", + "\t"]) + + def frame_sources(self, relative: str, index: int, count: int) -> list: + return [relative, f"{relative}__0"] if count == 1 else [f"{relative}__{index}"] + + def bitmap(self, tag: str) -> str: + """a for the bitmap tag, its frames drawn""" + group = self.tags.load(tag, "bitmap") + data, sequences = group["bitmap data"], group["bitmap group sequence"] + relative = tag.replace("\\", "/")[3:] + if sequences and sequences[0]["bitmap count"]: + first = sequences[0]["first bitmap index"] + indices = list(range(first, first + sequences[0]["bitmap count"])) + elif sequences and sequences[0]["sprites"]: + sys.exit(f"{tag}: sprites are not supported") + else: + indices = list(range(len(data))) + lines = [f"\t"] + for index in indices: + width, height = data[index]["width"], data[index]["height"] + name = self.frame_sources(relative, index, len(data))[-1] + if name in XBOX_FRAMES and not any((self.redraws / f"{source}.svg").is_file() + for source in self.frame_sources(relative, index, len(data))): + self.xbox_frames.append((name, tag, XBOX_FRAMES[name])) + lines.append(f"\t\t") + continue + png = f"ce/{relative}__{index}.png" + self.draw(relative, index, len(data), width, height, MENUS / png, self.shown.get(tag, [])) + self.pngs.append(png) + lines.append(f"\t\t") + lines.append("\t") + return "\n".join(lines) + + def draw(self, relative: str, index: int, count: int, width: int, height: int, output: Path, + shown: list) -> None: + output.parent.mkdir(parents=True, exist_ok=True) + for name in self.frame_sources(relative, index, count): + svg = self.redraws / f"{name}.svg" + if svg.is_file(): + copy = SVG_COPIES / f"{name}.svg" + copy.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(svg, copy) + self.svgs.append(copy) + scale = max(1, min(SCALE, MAXIMUM_SIZE // max(width, height))) + image = render_svg(copy, scale) + if image.shape[:2] != (height * scale, width * scale): + sys.exit(f"{svg}: {image.shape[1]}x{image.shape[0]}, not {scale}x {width}x{height}") + Image.fromarray(bleed(image), "RGBA").save(output, optimize=True) + return + # (no redraw: the placeholder, as large as fits, in the middle of where + # the PC version's picture is (only its bounds are read), else of the + # part the widgets show: a widget draws its bitmap from the top left, + # cut to its size) + name = self.frame_sources(relative, index, count)[-1] + image = Image.new("RGBA", (width * PLACEHOLDER_SCALE, height * PLACEHOLDER_SCALE), (0, 0, 0, 0)) + area = (0, 0, min([width] + [w for w, _ in shown]), min([height] + [h for _, h in shown])) + original = self.pictures_folder / f"{name}.png" if self.pictures_folder else None + if original and original.is_file(): + with Image.open(original) as picture: + if picture.size == (width, height): + bounds = picture.convert("RGBA").getchannel("A").point(lambda value: 255 if value > 16 else 0).getbbox() + if bounds: + area = (bounds[0], bounds[1], min(bounds[2], area[2]), min(bounds[3], area[3])) + area = tuple(value * PLACEHOLDER_SCALE for value in area) + picture = self.placeholder.copy() + picture.thumbnail((area[2] - area[0], area[3] - area[1]), Image.LANCZOS) + image.paste(picture, ((area[0] + area[2] - picture.width) // 2, (area[1] + area[3] - picture.height) // 2)) + image.save(output, optimize=True) + self.pictures.append((output.relative_to(MENUS).as_posix(), width, height, f"{name}.png")) + + +# ---------- the widgets + + +def reachable(tags: Tags) -> dict: + widgets, queue = {}, [MAIN_MENU] + while queue: + tag = queue.pop() + if tag in widgets: + continue + widget = tags.load(tag, "ui_widget_definition") + widgets[tag] = widget + for reference in ([child["widget tag"] for child in widget["child widgets"]] + + [handler["widget tag"] for handler in widget["event handlers"]] + + [conditional["widget tag"] for conditional in widget["conditional widgets"]] + + [widget["extended description widget"]]): + if reference and reference not in LEFT_OUT: + queue.append(reference) + return widgets + + +def bitmap_name(tag: str) -> str: + """a bitmap: the Xbox map's (its path) for those it has, else ours""" + return tag if tag in XBOX_BITMAPS else our_name(tag) + + +def widget_xml(tag: str, widget: dict, tags: Tags, functions: list, inputs: list, fonts: Counter, + children_of: dict, depth: int = 1) -> list: + """the , and its children's where it is their only parent""" + indent = "\t" * depth + bounds = widget["bounds"] + top, left, bottom, right = bounds + pairs = [ + ("name", our_name(tag)), + ("type", TYPES[widget["widget type"]] if widget["widget type"] else None), + ("controller", None if widget["controller index"] == 4 else str(widget["controller index"] + 1)), + ("left", left or None), ("top", top or None), ("width", right - left), ("height", bottom - top), + ("flags", flag_names(widget["flags"], WIDGET_FLAGS)), + ("auto_close", widget["milliseconds to auto close"] or None), + ("auto_close_fade", widget["milliseconds auto close fade time"] or None), + ("bitmap", bitmap_name(widget["background bitmap"]) if widget["background bitmap"] else None), + ] + if widget["text label unicode strings list"]: + pairs.append(("string_list", our_name(widget["text label unicode strings list"]))) + pairs.append(("string_index", widget["string list index"] or None)) + if widget["text font"]: + font = FONTS.get(widget["text font"], widget["text font"]) + fonts[widget["text font"]] += 1 + pairs.append(("font", font)) + alpha, red, green, blue = widget["text color"] + if (alpha, red, green, blue) != (0, 0, 0, 0): + pairs.append(("color", "#%02X%02X%02X%02X" % tuple(round(c * 255) for c in (alpha, red, green, blue)))) + pairs += [ + ("align", ALIGN[widget["justification"]] if widget["justification"] else None), + ("text_flags", flag_names(widget["flags 1"], TEXT_FLAGS)), + ("text_x", widget["horiz offset"] or None), ("text_y", widget["vert offset"] or None), + ("list_flags", flag_names(widget["flags 2"], LIST_FLAGS)), + ("header_bitmap", bitmap_name(widget["list header bitmap"]) if widget["list header bitmap"] else None), + ("footer_bitmap", bitmap_name(widget["list footer bitmap"]) if widget["list footer bitmap"] else None), + ("header_bounds", bounds_text(widget["header bounds"]) if any(widget["header bounds"]) else None), + ("footer_bounds", bounds_text(widget["footer bounds"]) if any(widget["footer bounds"]) else None), + ("description", our_name(widget["extended description widget"]) + if widget["extended description widget"] else None), + ] + patch = port_settings.WIDGET_PATCHES.get(our_name(tag), {}) + for key, value in patch.get("set", {}).items(): + pairs = [(k, v) for k, v in pairs if k != key] + [(key, value)] + lines = [f"{indent}"] + inner = indent + "\t" + for data in widget["game data inputs"]: + name = inputs[data["function"]] + lines.append(f"{inner}") + lines += [f"{inner}{line}" for line in patch.get("handlers", [])] + for handler in widget["event handlers"] if "handlers" not in patch else []: + flags = handler["flags"] + target = our_name(handler["widget tag"]) if handler["widget tag"] else None + handler_pairs = [("event", EVENTS[handler["event type"]])] + if flags >> 7 & 1: + name = functions[handler["function"]] + handler_pairs.append(("run", f"unwired {name}" if name in INCOMPATIBLE else name)) + if flags >> 10 & 1: + handler_pairs.append(("script", handler["script"])) + elif handler["script"]: + handler_pairs.append(("label", handler["script"])) + for bit, key in ((3, "open"), (8, "replace"), (6, "focus")): + if flags >> bit & 1: + handler_pairs.append((key, target)) + if flags >> 1 & 1: + handler_pairs += [("close", "other"), ("widget", target)] + if flags >> 5 & 1: + handler_pairs += [("reload", "other"), ("widget", target)] + for bit, pair in ((0, ("close", "current")), (2, ("close", "all")), (4, ("reload", "self")), + (9, ("back", "true")), (11, ("branch", "true"))): + if flags >> bit & 1: + handler_pairs.append(pair) + if handler["sound effect"]: + handler_pairs.append(("sound", handler["sound effect"])) + seen, unique = set(), [] + for key, value in handler_pairs: + if key not in seen: + seen.add(key) + unique.append((key, value)) + lines.append(f"{inner}") + for item in widget["search and replace functions"]: + function = REPLACE[item["replace function"]] if item["replace function"] < len(REPLACE) else None + lines.append(f"{inner}") + for conditional in widget["conditional widgets"]: + lines.append(f"{inner}") + for child in widget["child widgets"]: + child_tag = child["widget tag"] + if child_tag in LEFT_OUT: + continue + x, y = CHILD_OFFSETS.get((tag, child_tag), (child["horizontal offset"], child["vertical offset"])) + name = patch.get("swap", {}).get(our_name(child_tag), our_name(child_tag)) + lines += [f"{inner}{line}" for line in patch.get("insert_before", {}).get(our_name(child_tag), [])] + pairs = [("widget", name), ("x", x or None), ("y", y or None), + ("controller", str(child["custom controller index"] + 1) if child["flags"] & 1 else None)] + lines.append(f"{inner}") + lines += [f"{inner}{line}" for line in patch.get("children", [])] + lines.append(f"{indent}") + return lines + + +def engine_names(source: Path, start: str, count: int) -> list: + """the names a table of the game's functions gives them, in order""" + import re + text = source.read_text(encoding="latin-1") + text = text[text.index(start):] + return re.findall(r'"([^"\\]*)"', text[:text.index("}")])[:count] + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--tags", type=Path, required=True) + parser.add_argument("--definitions", type=Path, required=True) + parser.add_argument("--redraws", type=Path, required=True) + parser.add_argument("--placeholder", type=Path, required=True, help="the picture for frames with no redraw") + parser.add_argument("--pictures", type=Path, + help="the PC version's pictures, as PNGs by tag path: only where each is, to place its placeholder") + arguments = parser.parse_args() + tags = Tags(arguments.tags, arguments.definitions) + # (the names of the Xbox's own as the game has them, which the tags' own + # names give a little differently: "create&edit", "NULL") + functions = tags.options("UIEventHandlerReferenceFunction") + inputs = tags.options("UIGameDataInputReferenceFunction") + functions[:102] = engine_names(ROOT / "source/interface/ui_widget_event_handler_functions.c", + '\t{\n\t\t"NULL",', 102) + inputs[:41] = engine_names(ROOT / "port/linux/game/menu_tags.c", "game_data_input_names[] =", 41) + for folder in (CE, SVG_COPIES, MENUS / "port_svg"): + if folder.exists(): + shutil.rmtree(folder) + CE.mkdir(parents=True) + widgets = reachable(tags) + # the sizes the widgets show their bitmaps at + shown = defaultdict(list) + for widget in widgets.values(): + top, left, bottom, right = widget["bounds"] + size = (right - left, bottom - top) + if widget["background bitmap"]: + shown[widget["background bitmap"]].append(size) + for kind in ("header", "footer"): + if widget[f"list {kind} bitmap"]: + top, left, bottom, right = widget[f"{kind} bounds"] + shown[widget[f"list {kind} bitmap"]].append((right - left, bottom - top) if any(widget[f"{kind} bounds"]) + else size) + art = Art(tags, arguments.redraws, arguments.placeholder, shown, arguments.pictures) + fonts = Counter() + # the widgets, a file for each folder of their tags + folders = defaultdict(list) + for tag in sorted(widgets): + folder = our_name(tag).rsplit("/", 1)[0] if "/" in our_name(tag) else "shell" + # (the settings screens this port has its own of: tools/port_settings.py) + if not port_settings.replaced(folder): + folders[folder].append(tag) + string_lists, bitmaps = set(), set() + for widget in widgets.values(): + if widget["text label unicode strings list"]: + string_lists.add(widget["text label unicode strings list"]) + for key in ("background bitmap", "list header bitmap", "list footer bitmap"): + if widget[key] and widget[key] not in XBOX_BITMAPS: + bitmaps.add(widget[key]) + for folder, members in sorted(folders.items()): + lines = ['', + f""] + lines.append("" if MAIN_MENU not in members else f"") + for tag in members: + lines += widget_xml(tag, widgets[tag], tags, functions, inputs, fonts, {}) + lines.append("") + (CE / f"{folder.replace('/', '.')}.xml").write_text("\n".join(lines) + "\n") + for name, lines in [*port_settings.settings_files().items(), *port_settings.multiplayer_files().items()]: + (CE / name).write_text("\n".join(lines)) + # their strings + lines = ['', + "", ""] + for tag in sorted(string_lists): + strings = [text_value(item["string"]) for item in tags.load(tag, "unicode_string_list")["strings"]] + strings = port_settings.STRING_OVERRIDES.get(our_name(tag), strings) + lines.append(f"\t") + lines += [f"\t\t" for text in strings] + lines.append("\t") + lines.append("") + (CE / "strings.xml").write_text("\n".join(lines) + "\n") + # their bitmaps + lines = ['', + "", ""] + lines += [art.bitmap(tag) for tag in sorted(bitmaps)] + lines += [art.title(name, text) for name, text in sorted(port_settings.TITLES.items())] + lines.append("") + (CE / "bitmaps.xml").write_text("\n".join(lines) + "\n") + # what is not hand-drawn + report = [ + "# Menu pictures that are not redraws", + "", + "The menus' bitmaps are drawn from the high-res redraws (`svg/`, from ui-svg-handmade) where there is", + "one. The PC version's pictures that have none (3D renders, screenshots and logos) are not in this", + "repository.", + "", + "## From the Xbox's map", + "", + "These have the same pictures, in the same order, in the Xbox's `ui.map`, which the menus draw them", + "from (the player's own; nothing is shipped). `mp_map_grafix`: the Xbox's has the 13 Xbox maps' pictures", + "(the PC version's first 13) and its \"?\"; the PC version's other 8 are its own maps'.", + "", + ] + report += [f"- `{tag}`" for tag in sorted(XBOX_BITMAPS)] + report += [ + "", + "These frames are drawn from a frame of the Xbox map's bitmap of the same name, whose other frames differ:", + "the same picture, or for the profile settings the Xbox's picture for the same thing.", + "", + "| The PC version's frame | The Xbox's frame |", + "| --- | --- |", + ] + report += [f"| `{name}.png` | `{tag}` frame {index} |" for name, tag, index in sorted(art.xbox_frames)] + report += [ + "", + "## Placeholders, to be redrawn", + "", + "The Xbox's map has none of these (or other pictures under the name), so each frame is a placeholder", + "until it is redrawn.", + "", + "| File | Size | The PC version's picture |", + "| --- | --- | --- |", + ] + report += [f"| `{png}` | {w}x{h} | `{source}` |" for png, w, h, source in sorted(art.pictures)] + report += ["", f"{len(art.pictures)} of {len(art.pngs)} frames are placeholders.", + "", "The PC version's `ui\\gamespy` and `ui\\ticker` fonts are drawn with `ui\\small_ui`, which", + "the Xbox's map has.", ""] + (MENUS / "NON_HANDDRAWN.md").write_text("\n".join(report)) + # what is not wired + used = Counter() + for path in CE.glob("*.xml"): + import re + for name in re.findall(r'(?:run|input)="([^"]*)"', path.read_text()): + used[name] += 1 + pc_functions = set(functions[102:]) | set(inputs[41:]) + unwired = sorted((name[8:] if name.startswith("unwired ") else name, count) for name, count in used.items() + if name.startswith("unwired ") or (name in pc_functions and name not in WIRED)) + report = [ + "# Menu functions that are not wired yet", + "", + "The PC version's menus (`ce/`, from `tools/ce_menus.py`) run functions of the game's. These do nothing", + "yet (`port/linux/game/menu_functions.c`), so what they fill in (lists, values) is empty, and what they", + "do (save a setting, join a game) does not happen; the screens open and close as the PC version's.", + "", + "- The PC version's own functions, which the Xbox's game has not.", + "- The Xbox's functions that the PC version rewrote for its own widgets, written `unwired ` in the", + " files: run on the PC widgets, the Xbox's would stop the game.", + "", + "| Function | Uses | |", + "| --- | --- | --- |", + ] + report += [f"| `{name}` | {count} | {'Xbox function, PC widgets' if name in INCOMPATIBLE else 'PC function'} |" + for name, count in unwired] + report += ["", "Wired: " + ", ".join(f"`{name}`" for name in sorted(WIRED)) + ".", ""] + (MENUS / "UNWIRED.md").write_text("\n".join(report)) + files = sorted(path.relative_to(MENUS).as_posix() for path in MENUS.rglob("*.xml")) + files += sorted(art.pngs) + (MENUS / "menus.json").write_text(json.dumps({ + "comment": "The menus' files the game embeds (tools/ce_menus.py, tools/embed_assets.py).", + "files": files, + }, indent=1) + "\n") + size = sum((MENUS / name).stat().st_size for name in files) + print(f"{len(widgets)} widgets in {len(folders)} files, {len(string_lists)} string lists, {len(bitmaps)} bitmaps " + f"({len(art.pngs)} frames, {len(art.pictures)} not redrawn); {size // 1024} KB; fonts {dict(fonts)}") + + +if __name__ == "__main__": + main() diff --git a/tools/ci_build.py b/tools/ci_build.py index 0aaa4ee7..6e67e362 100644 --- a/tools/ci_build.py +++ b/tools/ci_build.py @@ -95,6 +95,9 @@ def main() -> int: # the text's fonts (port/assets/fonts), embedded in every build, whose # SIL Open Font License asks each copy to carry it shutil.copy2(ROOT / "port/assets/fonts/Overpass-OFL.txt", dist / "Overpass-OFL.txt") + # the menus' XML parser (port/third_party/expat), in every build, whose + # MIT license asks copies to carry its notice + shutil.copy2(ROOT / "port/third_party/expat/COPYING", dist / "expat-COPYING.txt") return 0 diff --git a/tools/embed_assets.py b/tools/embed_assets.py index 4c3888bb..81941df6 100644 --- a/tools/embed_assets.py +++ b/tools/embed_assets.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """Embeds the high-res HUD textures (port/assets/hud, made by tools/hud_assets.py), the menus' titles (port/assets/titles, made by -tools/title_assets.py) and the fonts the text is drawn with -(port/assets/fonts) in the game as C data: +tools/title_assets.py), the fonts the text is drawn with +(port/assets/fonts) and the menus' files (port/assets/menus, made by +tools/ce_menus.py) in the game as C data: python tools/embed_assets.py OUTPUT.c @@ -32,6 +33,8 @@ TITLE_ASSETS = Path("port/assets/titles") TITLE_LIST = TITLE_ASSETS / "titles.json" FONT_ASSETS = Path("port/assets/fonts") FONT_LIST = FONT_ASSETS / "fonts.json" +MENU_ASSETS = Path("port/assets/menus") +MENU_LIST = MENU_ASSETS / "menus.json" PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" @@ -53,13 +56,20 @@ def textures() -> List[tuple]: return result +def menu_files() -> List[str]: + """The menus' files menus.json lists, relative to their folder.""" + if not (ROOT / MENU_LIST).is_file(): + return [] + return json.loads((ROOT / MENU_LIST).read_text())["files"] + + def hud_asset_inputs() -> List[Path]: """The files the generated source is made from.""" - inputs = [listing for listing in (LAYOUT, TITLE_LIST, FONT_LIST) if (ROOT / listing).is_file()] + inputs = [listing for listing in (LAYOUT, TITLE_LIST, FONT_LIST, MENU_LIST) if (ROOT / listing).is_file()] if not inputs: return [] return [*inputs, *(folder / f"{asset['name']}.png" for folder, asset, _ in textures()), - *(FONT_ASSETS / name for name in font_files())] + *(FONT_ASSETS / name for name in font_files()), *(MENU_ASSETS / name for name in menu_files())] def hud_configure_inputs() -> List[Path]: @@ -67,7 +77,8 @@ def hud_configure_inputs() -> List[Path]: folders, for files added or removed), not each file, which a change of a list may rename or remove.""" inputs = [] - for folder, listing in ((HUD_ASSETS, LAYOUT), (TITLE_ASSETS, TITLE_LIST), (FONT_ASSETS, FONT_LIST)): + for folder, listing in ((HUD_ASSETS, LAYOUT), (TITLE_ASSETS, TITLE_LIST), (FONT_ASSETS, FONT_LIST), + (MENU_ASSETS, MENU_LIST)): if (ROOT / listing).is_file(): inputs += [folder, listing] return inputs @@ -159,6 +170,28 @@ def main() -> None: lines.append("\t{ 0 },") lines.append("};") lines.append(f"const unsigned int text_hires_embedded_count = {len(fonts)};") + lines.append("") + # the menus' files (menu_files.h) + lines.append('#include "menu_files.h"') + lines.append("") + menus = menu_files() + for index, name in enumerate(menus): + data = (ROOT / MENU_ASSETS / name).read_bytes() + if name.endswith(".png"): + png_size(data, name) + lines.append(f"static const unsigned int menu{index}[] = {{") + lines.extend(words(data)) + lines.append("};") + lines.append("") + lines.append("const struct menu_file_embedded menu_files_embedded[] =") + lines.append("{") + for index, name in enumerate(menus): + size = (ROOT / MENU_ASSETS / name).stat().st_size + lines.append(f'\t{{ "{name}", menu{index}, {size} }},') + if not menus: + lines.append("\t{ 0 },") + lines.append("};") + lines.append(f"const unsigned int menu_files_embedded_count = {len(menus)};") output = Path(sys.argv[1]) output.parent.mkdir(parents=True, exist_ok=True) text = "\n".join(lines) + "\n" diff --git a/tools/linux_build.py b/tools/linux_build.py index 6d45e6cb..ac0a22ef 100644 --- a/tools/linux_build.py +++ b/tools/linux_build.py @@ -114,6 +114,8 @@ GAME_FLAGS = [ # the TOML parser the platform layer reads config.toml with (port_config.c) TOML_DIR = Path("port/third_party/tomlc17") +EXPAT_DIR = Path("port/third_party/expat") +EXPAT_SOURCES = ("xmlparse.c", "xmlrole.c", "xmltok.c") KCP_DIR = Path("port/third_party/kcp") MUSL_MATH_DIR = Path("port/third_party/musl-math") # the self-updater's TLS (port/linux/src/posix_update.c) @@ -417,6 +419,7 @@ def generate_linux_build(n: Writer, sln: Any) -> None: f"-I{platform_dir}", f"-I{port_include}", f"-I{TOML_DIR}", + f"-I{EXPAT_DIR}", f"-I{KCP_DIR}", "-Isource -Isource/cseries", sdk_flags, @@ -453,6 +456,9 @@ def generate_linux_build(n: Writer, sln: Any) -> None: # the settings file's parser (port/third_party/tomlc17), with the # platform layer's ABI (its structs hold doubles) and nothing else add_object(TOML_DIR / "tomlc17.c", " ".join([abi, "-std=gnu11", "-w"])) + # the menus' XML parser (port/third_party/expat; menu_files.c) + for name in EXPAT_SOURCES: + add_object(EXPAT_DIR / name, " ".join([abi, "-std=gnu11", f"-I{EXPAT_DIR}", "-w"])) # internet play's reliable streams (port/third_party/kcp; p2p.c) add_object(KCP_DIR / "ikcp.c", " ".join([abi, "-std=gnu11", "-w"])) # the game's sin, pow and the rest, the same on every port diff --git a/tools/port_settings.py b/tools/port_settings.py new file mode 100644 index 00000000..099658e8 --- /dev/null +++ b/tools/port_settings.py @@ -0,0 +1,721 @@ +"""The port's settings screens, which tools/ce_menus.py writes in place of the +PC version's (port/assets/menus/ce): laid out as its are (rows of a label and +a spinner over its row backgrounds, a line of help for the row chosen, +Defaults, OK and Cancel), with what this port has to set instead of what the +PC version had (no connection speed or texture quality: a window or the full +screen, the frame rate, the keyboard's own controls). + +A row's spinner sets one of config.toml's settings (setting=, values=), or of +the profile being edited ("profile.": the controller's settings), by +port/linux/game/menu_functions.c: "port setting load" shows its value, +"port settings save" (OK) writes those changed and applies them, "port +settings defaults" shows the defaults. Controls Setup binds the keyboard and +mouse's controls, by the functions the PC version names for it. +""" + +from xml.sax.saxutils import quoteattr + +PE = "main_menu/settings_select/player_setup/player_profile_edit" +YES_NO = [("YES", "true"), ("NO", "false")] +ON_OFF = [("ON", "true"), ("OFF", "false")] +SENSITIVITIES = [(f"{value:g}", f"{value:g}") for value in (0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4)] +VOLUMES = [(str(step), f"{step / 10:g}") for step in range(11)] + +# each screen: its folder below PE, its screen's widget (the name the profile +# menu opens), its header (widget, bitmap), the row spacing, and its rows: +# (label, setting, [(shown, value)], help, platform) +SCREENS = { + "video_settings": { + "screen": "video_settings_screen", + "header": ("header_profile_video_settings", f"{PE}/video_settings/header_profile_video_settings"), + "spacing": 26, + "rows": [ + ("DISPLAY MODE:", "display.mode", + [("FULLSCREEN", "fullscreen"), ("BORDERLESS", "borderless"), ("WINDOWED", "windowed")], + "Fullscreen and borderless draw at the display's\nresolution; windowed, 640x480 scaled. F11: window.", + "desktop"), + ("WINDOW SIZE:", "display.window_scale", + [("640 x 480", "1"), ("1280 x 960", "2"), ("1920 x 1440", "3"), ("2560 x 1920", "4")], + "The window's size when windowed (its edges can\nalso be dragged).", "desktop"), + ("V-SYNC:", "display.vsync", ON_OFF, + "Wait for the display between frames, so that the\npicture never tears.", None), + ("FRAME RATE LIMIT:", "display.max_fps", + [("AUTO", "0"), ("30", "30"), ("60", "60"), ("120", "120"), ("144", "144"), ("165", "165"), + ("240", "240"), ("NONE", "-1")], + "With V-Sync off, the most frames a second. Auto:\ntwice the display's refresh rate.", "desktop"), + ("SMOOTH MOTION:", "display.interpolation", ON_OFF, + "Draw a frame for every display refresh, blending\nbetween the game's 30 ticks a second.", None), + ("INSTANT AIM:", "display.direct_camera", ON_OFF, + "In first person, turn the view the moment the\nmouse moves, not up to two ticks later.", "desktop"), + ("HIGH-RES HUD:", "display.high_res_hud", ON_OFF, + "Draw the HUD from the high-res redraws; off\ndraws the game's own pictures.", None), + ("HIGH-RES TEXT:", "display.high_res_text", ON_OFF, + "Draw text and titles with high-res fonts; off\ndraws the game's own.", None), + ], + }, + "mouse_settings": { + "screen": "mouse_settings_screen", + "header": ("header_profile_mouse_settings", f"{PE}/mouse_settings/header_profile_mouse_settings"), + "spacing": 30, + "rows": [ + ("HORIZONTAL SENSITIVITY:", "input.mouse_sensitivity", SENSITIVITIES, + "How fast the view turns side to side for the\nmouse's movement.", None), + ("VERTICAL SENSITIVITY:", "input.mouse_vertical_sensitivity", [("SAME", "0")] + SENSITIVITIES, + "How fast the view turns up and down; Same\nturns it as fast as side to side.", None), + ("INVERT VERTICAL AXIS:", "input.invert_mouse", YES_NO, + "Moving the mouse forward looks down.", None), + ("AIM ASSIST:", "input.mouse_aim_assist", ON_OFF, + "Slow and drag the view along with a target while\naiming with the mouse, as with a controller.", None), + ], + }, + "audio_settings": { + "screen": "audio_settings_screen", + "header": ("header_profile_audio_settings", f"{PE}/audio_settings/header_profile_audio_settings"), + "spacing": 30, + "rows": [ + ("MASTER VOLUME:", "audio.volume", VOLUMES, "The volume of everything.", None), + ("MUSIC VOLUME:", "audio.music_volume", VOLUMES, "The music's volume.", None), + ("EFFECTS VOLUME:", "audio.effects_volume", VOLUMES, + "The volume of every other sound: effects and\nspeech.", None), + ("SOUND:", "audio.enabled", ON_OFF, + "Play sound at all; from the next time the game\nstarts.", None), + ], + }, + "network_setup": { + "screen": "network_settings_screen", + "header": ("header_profile_network_settings", f"{PE}/network_setup/header_profile_network_settings"), + "spacing": 30, + "rows": [ + ("INTERNET PLAY:", "network.online", ON_OFF, + "Host and join games over the internet by invite\nlinks; off keeps to the local network.", None), + ("UPNP PORT FORWARDING:", "network.allow_upnp", ON_OFF, + "Let internet play ask the router to forward its\nport, for networks that stop connections.", None), + ("JOIN FROM CLIPBOARD:", "network.join_from_clipboard", ON_OFF, + "Join the game of an invite link copied before\nswitching to the game.", None), + ("CHECK FOR UPDATES:", "update.auto", ON_OFF, + "Look for a new version when the game starts.", None), + ("PLAYER NAMES:", "display.player_names", + [("ALL", "all"), ("ALLIES", "allies"), ("ENEMIES", "enemies"), ("NONE", "none")], + "In multiplayer, whose names are drawn above their\nheads.", None), + ("PLAYER NAME SIZE:", "display.player_name_scale", + [(f"{value:g}x", f"{value:g}") for value in (0.5, 0.75, 1, 1.25, 1.5, 2)], + "How large the players' names are drawn.", None), + ("SCOREBOARD LAYOUT:", "display.scoreboard_team_layout", [("TEAMS", "teams"), ("BY SCORE", "score")], + "A team game's scoreboard: a column for each team,\nor every player in order of score.", None), + ("SCOREBOARD PANEL:", "display.scoreboard_background", ON_OFF, + "Draw a panel behind the scoreboard, for clearer\ntext.", None), + ], + }, + "gamepad_setup": { + "screen": "gamepad_setup_screen", + "header": ("header_profile_controller_setting", f"{PE}/controller_edit/header_profile_controller_setting"), + "spacing": 30, + "rows": [ + ("LOOK SENSITIVITY:", "profile.look_sensitivity", [(str(step), str(step)) for step in range(1, 11)], + "How fast the right stick turns the view.", None), + ("INVERT LOOK:", "profile.invert_look", YES_NO, "Pushing the right stick up looks down.", None), + ("INVERT FLIGHT:", "profile.flight_inversion", YES_NO, + "Flying vehicles pitch as aircraft do: pushing up\nflies down.", None), + ("AUTO-CENTER LOOK:", "profile.autocenter", YES_NO, + "Level the view while walking.", None), + ("BUTTON LAYOUT:", "profile.button_preset", + [("DEFAULT", "0"), ("SWAP TRIGGERS", "1"), ("SWAP A, L TRIGGER", "2"), ("SWAP B, L TRIGGER", "3"), + ("SWAP B, R STICK", "4")], + "Which of the controller's buttons does what (not\nthe keyboard's: Controls Setup sets those).", None), + ("STICK LAYOUT:", "profile.joystick_preset", + [("DEFAULT", "0"), ("SOUTHPAW", "1"), ("LEGACY", "2"), ("LEGACY SOUTHPAW", "3")], + "Which stick moves and which looks.", None), + ("VIBRATION:", "profile.vibration", ON_OFF, "Rumble the controller.", None), + ("IN-GAME HELP:", "profile.ingame_help", ON_OFF, "Show the game's hints about its controls.", None), + ], + }, +} + +# Controls Setup: the keyboard and mouse's actions, in groups (the order of +# port/linux/game/menu_functions.c's table of them) +CONTROL_GROUPS = ["MOVEMENT", "WEAPONS", "ACTIONS"] +CONTROL_ROWS = 7 + +# the profile menu's words for what its items now open +STRING_OVERRIDES = { + f"{PE}/profile_edit_descriptions": [ + "Rename this profile.\\n\\n\\nProfile:", + "Choose the keys and mouse\\nbuttons for each action.\\n\\nProfile:", + "Set this profile's controller\\nsensitivity and layouts.\\n\\nProfile:", + "Adjust the mouse's sensitivity\\nand aiming.\\n\\nProfile:", + "Adjust the volume of the music\\nand of everything else.\\n\\nProfile:", + "Choose a window or the full\\nscreen, the frame rate and more.\\n\\nProfile:", + "Internet play, updates and the\\nmultiplayer HUD.\\n\\nProfile:", + "Change the current profile's\\nfree-for-all multiplayer color.\\n\\nProfile:", + "Halo: Combat Evolved, the Xbox\\ngame, on this computer.\\n\\nProfile:", + ], +} + + +def attributes(pairs: list) -> str: + return "".join(f" {key}={quoteattr(str(value))}" for key, value in pairs if value is not None) + + +def _widget(name: str, pairs: list, inner: list) -> list: + return [f"\t", *[f"\t\t{line}" for line in inner], "\t"] + + +def _strings(name: str, texts: list) -> list: + return [f"\t", + *[f"\t\t" for text in texts], "\t"] + + +def _button(name: str, caption: int, handlers: list) -> list: + pairs = [("type", "text"), ("width", 128), ("height", 24), ("bitmap", "bitmaps/text_button_background"), + ("string_list", "strings/common_button_captions"), ("string_index", caption), ("font", "ui\\small_ui"), + ("color", "#FFFFFFFF"), ("align", "center"), ("text_y", 2)] + return _widget(name, pairs, handlers + ['']) + + +def _screen(folder: str, spec: dict, rows: list, list_inputs: list, list_handlers: list, extra: list) -> list: + """a screen: its header, the list of rows (with the help line as its + description), and the buttons""" + base = folder if folder.startswith("main_menu/") else f"{PE}/{folder}" + header, header_bitmap = spec["header"] + lines = [] + lines += _widget(f"{base}/{spec['screen']}", + [("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child pause_game"), + ("bitmap", "bitmaps/gradient")], + ['', '', + f'', + f'']) + lines += _widget(f"{base}/{header}", [("controller", 1), ("left", 35), ("top", 11), ("width", 605), ("height", 59), + ("bitmap", header_bitmap)], []) + lines += _widget(f"{base}/help", [("type", "text"), ("controller", 1), ("left", 68), ("top", 350), ("width", 482), + ("height", 60), ("string_list", f"{base}/help_strings"), + ("font", "ui\\large_ui"), ("color", "#FFFFFFFF")], []) + children = [f'' for name in list_inputs] + list_handlers + for index, (row, platform) in enumerate(rows): + children.append(f'') + children.append(f'') + lines += _widget(f"{base}/options_menu", + [("type", "column_list"), ("width", 640), ("height", 480), + ("flags", "pass_unhandled_to_focused_child up_down_tabs_children"), + ("description", f"{base}/help")], children) + lines += _widget(f"{base}/button_bar", + [("type", "column_list"), ("width", 640), ("height", 28), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items")], + [f'', + f'', + '']) + lines += extra + return lines + + +def _setting_screen(folder: str, spec: dict) -> list: + base = f"{PE}/{folder}" + rows, extra = [], [] + for index, (label, setting, choices, _, platform) in enumerate(spec["rows"]): + key = setting.split(".", 1)[1] + row = f"{base}/op_{key}" + rows.append((row, platform)) + extra += _widget(row, [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF"), ("platform", platform)], + [f'', + f'']) + extra += _widget(f"{base}/{key}_label", + [("type", "text"), ("controller", 1), ("width", 300), ("height", 22), + ("string_list", f"{base}/labels"), ("string_index", index), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + extra += _widget(f"{base}/{key}_spinner", + [("type", "spinner"), ("left", 3), ("top", 2), ("width", 147), ("height", 20), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("strings", "|".join(shown for shown, _ in choices)), ("setting", setting), + ("values", "|".join(value for _, value in choices)), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 1), + ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), + ("header_bounds", "7 -6 19 0"), ("footer_bounds", "7 150 19 156")], + ['']) + extra += _button(f"{base}/button_defaults", 3, ['', + '']) + extra += _button(f"{base}/button_ok", 1, ['', + '']) + extra += _strings(f"{base}/labels", [label for label, *_ in spec["rows"]]) + # (the help of the row whose label is string n is n + 1: the buttons' is 0) + extra += _strings(f"{base}/help_strings", + [""] + [help_text.replace("\n", "\\n") for _, _, _, help_text, _ in spec["rows"]]) + return _screen(folder, spec, rows, ["port settings help"], [], extra) + + +def _controls_screen() -> list: + folder = "controls_setup" + base = f"{PE}/{folder}" + spec = {"screen": "controls_settings_screen", "spacing": 30, + "header": ("header_profile_controls_settings", f"{base}/header_profile_controls_settings")} + rows, extra = [(f"{base}/op_group", None)], [] + extra += _widget(f"{base}/op_group", [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', + f'']) + extra += _widget(f"{base}/group_label", + [("type", "text"), ("controller", 1), ("width", 200), ("height", 22), ("text", "SHOW:"), + ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + extra += _widget(f"{base}/group_spinner", + [("type", "spinner"), ("left", 3), ("top", 2), ("width", 297), ("height", 20), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("strings", "|".join(CONTROL_GROUPS)), ("font", "ui\\large_ui"), ("color", "#FF2896FF"), + ("align", "center"), ("text_y", 1), ("header_bitmap", "bitmaps/arrow_sm_left"), + ("footer_bitmap", "bitmaps/arrow_sm_right"), ("header_bounds", "7 -6 19 0"), + ("footer_bounds", "7 300 19 306")], []) + for index in range(CONTROL_ROWS): + row = f"{base}/op_command_{index + 1}" + rows.append((row, None)) + extra += _widget(row, [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + ['', + '', + '', + f'', + f'', + f'']) + extra += _widget(f"{base}/command_label", + [("type", "text"), ("controller", 1), ("width", 200), ("height", 22), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + extra += _widget(f"{base}/command_binding", + [("type", "text"), ("controller", 1), ("width", 152), ("height", 22), ("font", "ui\\small_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 5), ("text_flags", "no_focus_test")], + []) + extra += _button(f"{base}/button_defaults", 3, ['', + '']) + extra += _button(f"{base}/button_ok", 1, ['', + '']) + extra += _strings(f"{base}/help_strings", [ + "Choose what to show, with left and right.", + "Enter sets the key chosen; left and right choose\\nwhich of two. Then press the new key or button.", + "Press a key, a mouse button or turn the wheel...\\nEscape cancels, Delete clears it.", + ]) + return _screen(folder, spec, rows, ["controls update menu"], [''], + extra) + + +def settings_files() -> dict: + """the files of the port's screens, by their names in ce/""" + files = {} + for folder, spec in SCREENS.items(): + files[f"{PE}/{folder}".replace("/", ".") + ".xml"] = _setting_screen(folder, spec) + files[f"{PE}/controls_setup".replace("/", ".") + ".xml"] = _controls_screen() + return {name: ['', + "", + "", *lines, "", ""] for name, lines in files.items()} + + +def replaced(folder: str) -> bool: + """whether the PC version's folder of widgets is replaced by ours (and + those below it: the controls' advanced screen, the video test)""" + return any(folder == path or folder.startswith(f"{path}/") + for path in [*(f"{PE}/{name}" for name in [*SCREENS, "controls_setup"]), *REPLACED_FOLDERS]) + + +# ---------- multiplayer (PLAN.md section 11) + +MT = "main_menu/multiplayer_type_select" + +# a custom loadout's weapons (game_engine.h's _loadout_weapon_*) +LOADOUT_WEAPONS = ["NONE", "RANDOM", "ASSAULT RIFLE", "PISTOL", "SHOTGUN", "SNIPER RIFLE", "ROCKET LAUNCHER", + "PLASMA PISTOL", "PLASMA RIFLE", "NEEDLER", "FUEL ROD", "FLAMETHROWER"] + + +def LOADOUT_HELPS(slot: str) -> list: + """the helps of a custom loadout's weapon's values""" + helps = [f"No {slot} weapon.", f"A random {slot} weapon, of those the map has,\\neach time you spawn."] + for weapon in LOADOUT_WEAPONS[2:]: + helps.append(f"Everyone spawns with a {weapon.lower()} as their {slot}\\nweapon (where the map has one).") + return helps + + +STRING_OVERRIDES.update({ + f"{MT}/multiplayer_options": ["JOIN GAME", "CREATE GAME", "INTERNET", "LAN", "DIRECT LINK", "EDIT GAMETYPES", + "SERVER BROWSER"], + f"{MT}/multiplayer_option_descriptions": [ + "Browse the games on the\\nInternet.", + "Join a multiplayer game on\\nyour LAN.", + "Join a game by its invite link,\\nor one a Discord invite\\nreached.", + "Host a game on the Internet:\\nplayers join by its invite\\nlink or Discord.", + "Host a game on your LAN\\nonly.", + "Set all the attributes for your \\nmultiplayer gametypes and\\nkeep them for future use.", + ], + "main_menu/gametype_select/var_gametype_banks": ["STANDARD", "CUSTOM"], + # (the PC's weapon sets, then the Xbox's NO GRENADES: menu_functions.c's + # gametype_options maps them to the engine's) + "main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_labels": ["INFINITE GRENADES:", "WEAPON SET:", "VEHICLE SET:", "STARTING EQUIPMENT:", + "VEHICLE RESPAWN TIME:", "LOADOUT:", "PRIMARY WEAPON:", "SECONDARY WEAPON:"], + # (the helps of each option's values in turn: menu_functions.c's + # gametype_option_help) + "main_menu/settings_select/multiplayer_setup/item_options_edit/cap_item_options": [ + "Everyone has a limitless supply of grenades, which\\nmakes for loud, explosive and occasionally messy fun.", + "Each player can only carry up to 4 grenades of each\\ntype at any given time.", + "The map will contain whatever weapons the designers\\nplaced on it.", + "All the weapons on the map will be replaced by\\npistols.", + "All the weapons on the map will be replaced by\\nassault rifles and plasma rifles.", + "All the weapons on the map will be replaced by\\nplasma weapons.", + "The guns on the map will be replaced by weapons\\nwith sniper scopes.", + "Sniper rifles and pistols will not appear on the map.", + "All the weapons on the map will be replaced by\\nrocket launchers.", + "All the weapons on the map will be replaced by\\nshotguns.", + "All the weapons on the map are only effective at\\nshort ranges.", + "There are no weapons on the map that were made in\\nCovenant sweat shops.", + "Only weapons made by the Covenant will appear\\nin the map.", + "The flamethrower and fuel rod gun will not appear\\nin the map.", + "Only the most devastating and dangerous of weapons\\nwill appear in the map.", + "The map's weapons, without any grenades.", + "You will start the game with the weapon set that the \\nmap designers custom tailored for it.", + "Use the generic starting weapons across all maps.", + "The weapons follow the weapon set above.", + "Everyone starts with the primary and secondary\\nweapons below; the map's weapons are its own.", + *LOADOUT_HELPS("primary"), + *LOADOUT_HELPS("secondary"), + ], + "main_menu/settings_select/multiplayer_setup/item_options_edit/var_weapon_set": [ + "NORMAL", "PISTOLS", "RIFLES", "PLASMA WEAPONS", "SNIPER", "NO SNIPING", "ROCKET LAUNCHERS", "SHOTGUNS", + "SHORT RANGE", "HUMAN", "COVENANT", "CLASSIC", "HEAVY WEAPONS", "NO GRENADES"], +}) + +# changes to the PC version's widgets (by our names): attributes set, all +# their handlers replaced, children added +WIDGET_PATCHES = { + # (straight to their screens: no "checking for updates" dialog, which + # asked the PC version's servers) + f"{MT}/multiplayer_type_join_internet_item": {"set": {"string_index": 6}, "handlers": [ + f'', + f'', + '', + ]}, + f"{MT}/multiplayer_type_create_internet_item": {"handlers": [ + '', + f'', + '', + ]}, + f"{MT}/multiplayer_type_join_direct_item": {"handlers": [ + f'', + f'', + '', + ]}, + f"{MT}/join_game/header_join_game": {"children": [ + f'', + f'', + ]}, + # (the small font's line does not fit below their 4 units of offset) + f"{MT}/join_game/ticker_player_info": {"set": {"text_y": 1}}, + f"{MT}/join_game/ticker_rules_info": {"set": {"text_y": 1}}, + # (Item Options' loadout rows, over its buttons) + "main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_menu": {"insert_before": { + "main_menu/settings_select/multiplayer_setup/item_options_edit/item_button_bar": [ + f'', + f'', + f'', + ]}}, + # (the PC's Vehicles row's Start opened Item Options) + "main_menu/settings_select/multiplayer_setup/playlist_edit/playlist_edit_vehicles_list_item": {"handlers": [ + '', + '', + '', + ]}, + # (Direct Link's clipboard button in the place of the PC version's + # Refresh, which this port has not: the lists update themselves; "swap" + # puts another widget in a child's place) + f"{MT}/join_game/join_game_button_bar": {"swap": { + f"{MT}/join_game/join_game_button_refresh": f"{MT}/join_game/button_clipboard", + }}, +} + +# the titles this port has that the PC version has not, set as its headers +# are (port/assets/menus/port_svg): bitmap name, text +TITLES = { + f"{MT}/join_game/header_server_browser": "SERVER BROWSER", + f"{MT}/join_game/header_direct_link": "DIRECT LINK", + f"{MT}/lobby/header_lobby": "GAME LOBBY", +} + + +def title_backdrop(width: float) -> str: + """a header's dark backdrop, as the PC headers' redraws have it, for text + width units wide (the text is set over it in OpenCE: ce_menus.py)""" + return "\n".join([ + '', + " ", + ' ', + ' ', " ", + ' ', + ' ', " ", " ", + ' ', + f' ', + " ", "", ""]) + + +# where the PC headers' text is (units of their 512x64), and its colour +TITLE_LEFT, TITLE_TOP, TITLE_CAP = 30.68, 33.5, 21.5 +TITLE_COLOR = (0x29, 0x95, 0xFD, 255) + + +def _header(name: str, bitmap: str, platform=None) -> list: + return _widget(name, [("controller", 1), ("left", 35), ("top", 11), ("width", 363), ("height", 59), + ("bitmap", bitmap), ("platform", platform)], []) + + +# a text field's B and Back do nothing of their own (the engine would go +# back from the field): its screen's (SCREEN_BACK) cancel the edit, else +# leave +FIELD_BACK = ['', ''] +SCREEN_BACK = ['', ''] + + +def _value_row(base: str, key: str, label_index: int, run: str) -> list: + """a row of a label and a value the player sets (A edits it)""" + lines = _widget(f"{base}/op_{key}", [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', f'', *FIELD_BACK, + '', + f'', f'']) + lines += _widget(f"{base}/{key}_label", [("type", "text"), ("controller", 1), ("width", 230), ("height", 22), + ("string_list", f"{base}/labels"), ("string_index", label_index or None), + ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("text_x", 13), + ("text_y", 4)], []) + lines += _widget(f"{base}/{key}_value", [("type", "text"), ("controller", 1), ("width", 280), ("height", 22), + ("font", "ui\\small_ui"), ("color", "#FF2896FF"), ("align", "center"), + ("text_y", 5), ("text_flags", "no_focus_test")], []) + return lines + + +def _join_game_extras() -> list: + """the browser's added titles and Direct Link's clipboard button""" + base = f"{MT}/join_game" + lines = _header(f"{base}/header_server_browser", f"{base}/header_server_browser") + lines += _header(f"{base}/header_direct_link", f"{base}/header_direct_link") + lines += _widget(f"{base}/button_clipboard", [("type", "text"), ("width", 128), ("height", 24), + ("bitmap", "bitmaps/text_button_background"), + ("text", "PASTE LINK"), ("font", "ui\\small_ui"), + ("color", "#FFFFFFFF"), ("align", "center"), ("text_y", 2)], + ['', '', + '']) + return lines + + +def _server_settings() -> list: + """Create Game's server settings (in the PC version's place): the game's + name, the most players (up to the port's 128), and its invite link""" + base = f"{MT}/server_settings" + spec = {"screen": "server_settings_screen", "spacing": 30, + "header": ("header_server_settings", f"{base}/header_server_settings")} + rows, extra = [], [] + extra += _value_row(base, "server_name", 0, "ss edit server name") + rows.append((f"{base}/op_server_name", None)) + extra += _widget(f"{base}/op_max_players", [("width", 512), ("height", 28), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', + f'']) + extra += _widget(f"{base}/max_players_label", [("type", "text"), ("controller", 1), ("width", 300), + ("height", 22), ("string_list", f"{base}/labels"), + ("string_index", 1), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + extra += _widget(f"{base}/max_players_spinner", + [("type", "spinner"), ("left", 3), ("top", 2), ("width", 147), ("height", 20), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("strings", "|".join(str(count) for count in MAXIMUM_PLAYERS)), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 1), + ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), + ("header_bounds", "7 -6 19 0"), ("footer_bounds", "7 150 19 156")], []) + rows.append((f"{base}/op_max_players", None)) + extra += _value_row(base, "invite", 2, "ss copy invite") + rows.append((f"{base}/op_invite", None)) + # the gametype's options for this game (the gametype editor's screens, + # editing a copy of the gametype chosen: "port setup edit") + for index, (key, screen) in enumerate(SETUP_OPTION_SCREENS): + target = f"main_menu/settings_select/multiplayer_setup/{screen}" + extra += _widget(f"{base}/op_{key}", [("width", 512), ("height", 28), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', + f'', + '', + f'', f'']) + extra += _widget(f"{base}/{key}_label", [("type", "text"), ("controller", 1), ("width", 230), ("height", 22), + ("string_list", f"{base}/labels"), ("string_index", 3 + index), + ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("text_x", 13), + ("text_y", 4)], []) + extra += _widget(f"{base}/{key}_value", [("type", "text"), ("controller", 1), ("width", 280), ("height", 22), + ("font", "ui\\small_ui"), ("color", "#FF2896FF"), ("align", "center"), + ("text_y", 5), ("text_flags", "no_focus_test")], []) + rows.append((f"{base}/op_{key}", None)) + extra += _button(f"{base}/button_defaults", 3, []) + extra += _widget(f"{base}/button_ok", [("type", "text"), ("width", 128), ("height", 24), + ("bitmap", "bitmaps/text_button_background"), ("text", "START GAME"), + ("font", "ui\\small_ui"), ("color", "#FFFFFFFF"), ("align", "center"), + ("text_y", 2)], + [f'', + f'', + '']) + extra += _strings(f"{base}/labels", ["GAME NAME:", "MAXIMUM PLAYERS:", "INVITE LINK:", "GAME TYPE:", + "PLAYER OPTIONS:", "ITEM OPTIONS:", "VEHICLE OPTIONS:", "INDICATOR OPTIONS:", + "TEAMPLAY OPTIONS:"]) + extra += _strings(f"{base}/help_strings", [ + "", + "The name the game shows in the lists of games.\\nEnter changes it.", + "The most players the game takes (up to 128).", + "Players join this game by its invite link, or your\\nDiscord invite. Enter copies the link again.", + "The gametype and its rules, for this game.", + "Lives, health, shields and respawning, for this game.", + "Weapons, grenades and starting equipment, for\\nthis game.", + "Each team's vehicles and their respawn time, for\\nthis game.", + "The motion tracker and nav points, for this game.", + "Friendly fire and team balance, for this game.", + ]) + lines = _screen(base, spec, rows, ["server settings update"], + [''], extra) + # (no Defaults: the bar is START and CANCEL; B cancels the name's edit + # first) + lines = [line.replace(f'', "") for line in lines] + lines = [line.replace('', SCREEN_BACK[0]) + .replace('', SCREEN_BACK[1]) for line in lines] + return lines + + +MAXIMUM_PLAYERS = [2, 4, 8, 12, 16, 24, 32, 48, 64, 96, 128] +# Server Setup's rows of the gametype's options: the gametype editor's +# screens +SETUP_OPTION_SCREENS = [ + ("game_type", "playlist_edit/game_type_select/gametype_select_screen"), + ("player_options", "player_options_edit/player_options_screen"), + ("item_options", "item_options_edit/item_options_screen"), + ("vehicle_options", "vehicle_options_edit/vehicle_options_screen"), + ("indicator_options", "indicator_options_edit/indicator_options_screen"), + ("team_options", "teamplay_options_edit/teamplay_options_screen"), +] + + +def _lobby() -> list: + """the lobby the host's and the joining players wait in (the Xbox's + pregame's functions, which need no widget of theirs): up to the port's + 128 players, scrolling; the game's map and gametype; the countdown""" + base = f"{MT}/lobby" + lines = _widget(f"{base}/lobby_screen", [("width", 640), ("height", 480), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/gradient")], + ['', + '', + '', + '', + '', + f'', + f'']) + lines += _header(f"{base}/header_lobby", f"{base}/header_lobby") + rows = [f'' + for index in range(11)] + lines += _widget(f"{base}/lobby_list", [("type", "column_list"), ("width", 640), ("height", 480), + ("flags", "pass_unhandled_to_focused_child up_down_tabs_children"), + ("description", f"{base}/lobby_desc")], + ['', '', + '', *rows, + f'']) + lines += _widget(f"{base}/lobby_desc", [("width", 640), ("height", 480)], + ['', + f'']) + lines += _widget(f"{base}/lobby_right_item", [("controller", 1), ("left", 406), ("top", 75), ("width", 162), + ("height", 326), + ("bitmap", "bitmaps/spinner_list_3_wide_item_background")], + [f'', f'', + f'']) + lines += _widget(f"{base}/lobby_map_pic", [("controller", 1), ("left", 419), ("top", 87), ("width", 140), + ("height", 114), ("bitmap", "ui\\shell\\bitmaps\\mp_map_grafix")], []) + lines += _widget(f"{base}/lobby_map_name", [("type", "text"), ("controller", 1), ("left", 417), ("top", 204), + ("width", 146), ("height", 43), ("string_list", "main_menu/mp_map_list"), + ("font", "ui\\large_ui"), ("color", "#FF2896FF")], []) + lines += _widget(f"{base}/lobby_game_data", [("type", "text"), ("controller", 1), ("left", 417), ("top", 250), + ("width", 146), ("height", 144), ("font", "ui\\small_ui"), + ("color", "#FF2896FF")], []) + lines += _widget(f"{base}/lobby_button_bar", [("type", "column_list"), ("width", 640), ("height", 28), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items")], + [f'', + f'', + f'']) + for key, caption, handlers in ( + ("team", "SWITCH TEAM", ['', '']), + ("start", "START NOW", ['', + '']), + ("leave", "LEAVE", ['', + ''])): + lines += _widget(f"{base}/lobby_button_{key}", [("type", "text"), ("width", 128), ("height", 24), + ("bitmap", "bitmaps/text_button_background"), + ("text", caption), ("font", "ui\\small_ui"), + ("color", "#FFFFFFFF"), ("align", "center"), ("text_y", 2)], + handlers + ['']) + # a game under way's lobby, before joining it (the browser's rows of + # games in progress): what its advertisement tells, JOIN GAME + lines += _widget(f"{base}/preview_screen", [("width", 640), ("height", 480), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/gradient")], + ['', '', + '', + f'', + f'']) + lines += _widget(f"{base}/preview_list", [("type", "column_list"), ("width", 640), ("height", 480), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("description", f"{base}/lobby_desc")], + ['', + f'', + f'']) + lines += _widget(f"{base}/preview_status", [("type", "text"), ("controller", 1), ("width", 360), ("height", 300), + ("font", "ui\\large_ui"), ("color", "#FF2896FF")], []) + lines += _widget(f"{base}/preview_button_bar", [("type", "column_list"), ("width", 640), ("height", 28), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items")], + [f'', + f'']) + for key, caption, run in (("join", "JOIN GAME", "port lobby preview join"), ("back", "BACK", "mouse emit back event")): + lines += _widget(f"{base}/preview_button_{key}", [("type", "text"), ("width", 128), ("height", 24), + ("bitmap", "bitmaps/text_button_background"), + ("text", caption), ("font", "ui\\small_ui"), + ("color", "#FFFFFFFF"), ("align", "center"), ("text_y", 2)], + [f'', f'', + '']) + return lines + + +def _item_options_extras() -> list: + """Item Options' loadout rows: CATEGORY (the weapon set) or CUSTOM (each + player's primary and secondary weapons)""" + base = "main_menu/settings_select/multiplayer_setup/item_options_edit" + lines = [] + for index, (key, strings) in enumerate((("loadout", "var_loadout"), ("primary_weapon", "var_loadout_weapon"), + ("secondary_weapon", "var_loadout_weapon"))): + lines += _widget(f"{base}/op_{key}", [("width", 512), ("height", 28), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', + f'']) + lines += _widget(f"{base}/{key}_label", [("type", "text"), ("controller", 1), ("width", 300), ("height", 22), + ("string_list", f"{base}/item_options_labels"), + ("string_index", 5 + index), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + lines += _widget(f"{base}/{key}_spinner", + [("type", "spinner"), ("top", 2), ("width", 206), ("height", 20), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("string_list", f"{base}/{strings}"), ("font", "ui\\large_ui"), ("color", "#FF2896FF"), + ("align", "center"), ("text_y", 4), ("list_flags", "items_from_strings"), + ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), + ("header_bounds", "7 -13 19 -7"), ("footer_bounds", "7 208 19 214")], + ['']) + lines += _strings(f"{base}/var_loadout", ["CATEGORY", "CUSTOM"]) + lines += _strings(f"{base}/var_loadout_weapon", LOADOUT_WEAPONS) + return lines + + +def multiplayer_files() -> dict: + """the port's multiplayer widgets: the browser's additions, the server + settings, the lobby""" + head = ['', + "", ""] + return { + f"{MT}/join_game".replace("/", ".") + ".port.xml": head + _join_game_extras() + ["", ""], + f"{MT}/server_settings".replace("/", ".") + ".xml": head + _server_settings() + ["", ""], + f"{MT}/lobby".replace("/", ".") + ".xml": head + _lobby() + ["", ""], + "main_menu/settings_select/multiplayer_setup/item_options_edit".replace("/", ".") + ".port.xml": head + _item_options_extras() + ["", ""], + } + + +REPLACED_FOLDERS = [f"{MT}/server_settings"] diff --git a/tools/test_linux_port.py b/tools/test_linux_port.py index 89fbcc30..ab004734 100644 --- a/tools/test_linux_port.py +++ b/tools/test_linux_port.py @@ -161,3 +161,144 @@ def test_game_sources_leave_out_the_excluded(tmp_path, monkeypatch): "defines": ["DEBUG"], "include_dirs": ["source", "source/saved games"]}} assert linux_build.game_sources(config) == [Path("source/a.c"), Path("source/zlib/b.c")] assert linux_build.game_defines_and_includes(config) == '-DDEBUG -Isource -I"source/saved games"' + + +# ---------- menus (port/assets/menus) + + +MENUS = Path(__file__).resolve().parent.parent / "port/assets/menus" +MENU_ATTRIBUTES = { + "menus": {"root"}, + "bitmap": {"name", "width", "height", "frames", "platform"}, + "frame": {"png", "map", "index", "width", "height", "platform"}, + "strings": {"name", "platform"}, + "string": {"text", "platform"}, + "widget": {"name", "type", "controller", "flags", "bitmap", "text", "strings", "values", "setting", "font", + "color", "align", "description", "string_list", "list_flags", "text_flags", "header_bitmap", + "footer_bitmap", "header_bounds", "footer_bounds", "child_controller", "x", "y", "left", "top", + "width", "height", "text_x", "text_y", "auto_close", "auto_close_fade", "string_index", "platform"}, + "child": {"widget", "controller", "x", "y", "platform"}, + "on": {"event", "run", "script", "open", "replace", "close", "widget", "focus", "reload", "sound", "otherwise", + "label", "back", "branch", "platform"}, + "data": {"input", "platform"}, + "conditional": {"widget", "if_failed", "platform"}, + "replace": {"search", "function", "platform"}, +} + + +def c_strings(path: Path, start: str, end: str) -> list: + """the string literals of a C file between two markers""" + text = path.read_text(encoding="latin-1") + text = text[text.index(start):] + return re.findall(r'"([^"\\]*)"', text[:text.index(end)]) + + +# menu_files.c's attributes read as whole numbers (its tables' { name, NULL, &long }) +MENU_INTEGER_ATTRIBUTES = {"auto_close", "auto_close_fade", "height", "index", "left", "string_index", "text_x", + "text_y", "top", "width", "x", "y"} + + +def test_menus_are_well_formed(): + """What port/linux/src/menu_files.c and port/linux/game/menu_tags.c check + when the game loads them, but the map's own names (paths with backslashes), + which only the map has.""" + import json + import xml.etree.ElementTree as ElementTree + + root = MENUS.parent.parent.parent + tags = root / "port/linux/game/menu_tags.c" + events = c_strings(tags, "event_names[] =", "};") + flags = c_strings(tags, "widget_flag_names[] =", "};") + functions = set(c_strings(root / "source/interface/ui_widget_event_handler_functions.c", '\t{\n\t\t"NULL",', "}")) + functions |= set(c_strings(tags, "port_function_names[] =", "};")) + inputs = set(c_strings(tags, "game_data_input_names[] =", "};")) + inputs |= set(c_strings(tags, "port_game_data_input_names[] =", "};")) + listed = json.loads((MENUS / "menus.json").read_text())["files"] + files = sorted(MENUS.rglob("*.xml")) + assert sorted(path.relative_to(MENUS).as_posix() for path in files) == \ + sorted(name for name in listed if name.endswith(".xml")) + widgets, bitmaps, strings, references, roots = {}, {}, {}, [], [] + for path in files: + tree = ElementTree.parse(path) + assert tree.getroot().tag == "menus", path + if tree.getroot().get("root"): + roots.append(tree.getroot().get("root")) + for element in tree.getroot().iter(): + where = f"{path.name}: <{element.tag} {element.get('name', '')}>" + assert element.tag in MENU_ATTRIBUTES, where + assert set(element.attrib) <= MENU_ATTRIBUTES[element.tag], where + assert element.get("platform") in (None, "desktop", "android"), where + # (menu_files.c's whole numbers, which the tags keep in shorts, + # its true/false attributes, and no text but whitespace outside + # s) + for attribute in MENU_INTEGER_ATTRIBUTES & set(element.attrib): + value = element.get(attribute) + assert re.fullmatch(r"-?[0-9]+", value) and -32768 <= int(value) <= 32767, f"{where} {attribute}" + for attribute in ("back", "branch", "if_failed"): + assert element.get(attribute) in (None, "true", "false"), f"{where} {attribute}" + assert not (element.text or "").strip() and not (element.tail or "").strip(), where + if element.tag == "bitmap": + bitmaps[element.get("name")] = element + for frame in element.iter("frame"): + assert (frame.get("png") is None) != (frame.get("map") is None), where + if frame.get("map"): + assert "\\" in frame.get("map") and int(frame.get("index")) >= 0, where + continue + assert frame.get("png") in listed, where + with (MENUS / frame.get("png")).open("rb") as png: + header = png.read(24) + width, height = int.from_bytes(header[16:20], "big"), int.from_bytes(header[20:24], "big") + logical = int(frame.get("width")), int(frame.get("height")) + assert width % logical[0] == 0 and width // logical[0] == height // logical[1], where + elif element.tag == "strings": + strings[element.get("name")] = element + elif element.tag == "widget": + name = element.get("name") + assert name and name not in widgets, where + widgets[name] = element + assert element.get("type", "container") in ("container", "text", "spinner", "column_list"), where + assert set(element.get("flags", "").split()) <= set(flags), where + children = element.findall("widget") + element.findall("child") + if element.get("type") == "spinner": + assert len(children) in (0, 1, 3), where + if element.get("strings") or "items_from_strings" in element.get("list_flags", ""): + assert element.get("type") == "spinner" and not children, where + if element.get("setting"): + assert len(element.get("strings").split("|")) == len(element.get("values").split("|")), where + for attribute in ("bitmap", "header_bitmap", "footer_bitmap"): + references.append((where, bitmaps, element.get(attribute))) + references.append((where, strings, element.get("string_list"))) + references.append((where, widgets, element.get("description"))) + elif element.tag == "child" or element.tag == "conditional": + references.append((where, widgets, element.get("widget"))) + elif element.tag == "on": + assert set(element.get("event").split()) <= set(events), where + run = element.get("run") + assert run is None or run in functions or run.startswith("unwired "), where + assert element.get("otherwise") is None or run, where + for attribute in ("open", "replace", "focus", "widget", "otherwise"): + references.append((where, widgets, element.get(attribute))) + elif element.tag == "data": + assert element.get("input") in inputs or element.get("input").startswith("unwired "), where + assert all(root_name in widgets for root_name in roots) + for where, names, name in references: + if name and "\\" not in name: + assert name in names, f"{where} {name}" + + +def test_menu_settings_exist(): + """Every setting a menu's spinner sets is one of config.toml's + (port/linux/src/port_config.c) or of the profile (menu_functions.c), and + the keyboard's controls are the same in the settings, in the controls' + screen and in the input code.""" + root = MENUS.parent.parent.parent + config = (root / "port/linux/src/port_config.c").read_text() + functions = (root / "port/linux/game/menu_functions.c").read_text() + known = set(re.findall(r'^\t\{ "([a-z_]+\.[a-z_]+)", _config_', config, re.M)) + profile = set(re.findall(r'\{ "(profile\.[a-z_]+)", \d', functions)) + for path in (MENUS / "ce").glob("*.xml"): + for setting in re.findall(r'setting="([^"]+)"', path.read_text()): + assert setting in known | profile, f"{path.name}: {setting}" + controls = set(re.findall(r'"(controls\.[a-z_]+)"', (root / "port/linux/src/xinput_sdl.c").read_text())) + assert controls == {name for name in known if name.startswith("controls.")} + assert controls == set(re.findall(r'\{ "(controls\.[a-z_]+)", L"', functions)) diff --git a/tools/windows_build.py b/tools/windows_build.py index 603d76c7..8df246dd 100644 --- a/tools/windows_build.py +++ b/tools/windows_build.py @@ -49,6 +49,8 @@ SDL_DIR = THIRD_PARTY / f"SDL3-{SDL_VERSION}" # frame chain. # the TOML parser the platform layer reads config.toml with (port_config.c) TOML_DIR = Path("port/third_party/tomlc17") +EXPAT_DIR = Path("port/third_party/expat") +EXPAT_SOURCES = ("xmlparse.c", "xmlrole.c", "xmltok.c", "random_rand_s.c") KCP_DIR = Path("port/third_party/kcp") @@ -353,6 +355,7 @@ def generate_windows_build(n: Writer, sln: Any) -> None: f"-I{linux_platform}", f"-I{PORT_DIR / 'include'}", f"-I{TOML_DIR}", + f"-I{EXPAT_DIR}", f"-I{KCP_DIR}", # halo_linux_winsock_names.h, but not the Linux build's C runtime # wrappers next to it @@ -392,6 +395,10 @@ def generate_windows_build(n: Writer, sln: Any) -> None: # the settings file's parser (port/third_party/tomlc17), with the # platform layer's ABI and nothing else add_object(TOML_DIR / "tomlc17.c", " ".join([abi, "-std=gnu11", "-w"])) + # the menus' XML parser (port/third_party/expat; menu_files.c), with + # its hash salt from rand_s + for name in EXPAT_SOURCES: + add_object(EXPAT_DIR / name, " ".join([abi, "-std=gnu11", f"-I{EXPAT_DIR}", "-w"])) # internet play's reliable streams (port/third_party/kcp; p2p.c) add_object(KCP_DIR / "ikcp.c", " ".join([abi, "-std=gnu11", "-w"])) # the game's sin, pow and the rest, the same on every port