diff --git a/CHANGELOG.md b/CHANGELOG.md index bbba40ae8..8a4d54436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ https://github.com/jcorporation/myMPD/ *** +## myMPD v19.0.0 (2024-12-15) + +This is a small maintenance release. It ships support for the newest MPD 0.24 protocol commands and enhancements for user defined stickers. + +### Scripting + +- The deprecated `var_` entries are remove from `mympd_env`, use the subtable `var`. + +### API changes + +- MYMPD_API_STICKER_NAMES: respect type parameter + +### Changelog + +- Feat: Support "stickernamestypes" command (MPD 0.24) #1092 +- Feat: Support "tagtypes reset" command (MPD 0.24) #1367 +- Feat: Generate smart playlists by user defined song stickers #1345 +- Feat: Show user defined stickers in lists #1368 +- Feat: New Lua method `mympd.api_partition` #1387 +- Upd: Improve lists layout +- Upd: libmympdclient 1.0.32 +- Upd: bootstrap.native 5.1 #1372 +- Upd: Improve local playback +- Upd: Mongoose 7.16 #1375 +- Upd: Improve http client +- Upd: sds to current master +- Upd: Remove IntersectionObserver, use image tag with lazy loading +- Fix: Support "contains" and "starts_with" sticker operators +- Fix: Increase max header count from 30 to 50 +- Fix: rpm dependency - whiptail is in package newt #1381 + +*** + ## myMPD v18.2.2 (2024-11-21) This is a small bug fix release. @@ -80,7 +113,6 @@ This is a small maintenance release. - Upd: Translations - Upd: Add Lua integer sanity checks - Upd: Mongoose to current master -- Upd: translations - Fix: Contextmenu for songs #1356 *** diff --git a/CMakeLists.txt b/CMakeLists.txt index 39e136323..0474fe9bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ cmake_policy(SET CMP0003 NEW) # myMPD is written in C # supported compilers: gcc, clang project(mympd - VERSION 18.2.2 + VERSION 19.0.0 LANGUAGES C ) @@ -20,6 +20,7 @@ message("Cmake version: ${CMAKE_VERSION}") message("Cmake src dir: ${PROJECT_SOURCE_DIR}") message("Cmake build dir: ${CMAKE_CURRENT_BINARY_DIR}") message("Cmake build type: ${CMAKE_BUILD_TYPE}") +message("Cmake generator: ${CMAKE_GENERATOR}") message("Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}") message("CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}") message("CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}") @@ -262,6 +263,8 @@ endif() # configure some files - version and path information configure_file(src/compile_time.h.in "${PROJECT_BINARY_DIR}/compile_time.h") +configure_file(src/web_server/embedded_files.c.in "${PROJECT_BINARY_DIR}/embedded_files.c") +configure_file(src/scripts/lualibs.c.in "${PROJECT_BINARY_DIR}/lualibs.c") configure_file(cmake/Install.cmake.in cmake/Install.cmake @ONLY) configure_file(contrib/initscripts/mympd.service.system.in contrib/initscripts/system/mympd.service @ONLY) configure_file(contrib/initscripts/mympd.service.user.in contrib/initscripts/user/mympd.service @ONLY) @@ -272,6 +275,7 @@ configure_file(contrib/initscripts/mympd.freebsdrc.in contrib/initscripts/mympd. # global compile options add_compile_options( "-DMPACK_HAS_CONFIG=1" + "-DMG_MAX_HTTP_HEADERS=50" ) # set myMPD specific debug define if(CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/Doxyfile b/Doxyfile index f947eb82a..f85ae4d38 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1250,46 +1250,6 @@ USE_HTAGS = NO VERBATIM_HEADERS = YES -# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the -# clang parser (see: -# http://clang.llvm.org/) for more accurate parsing at the cost of reduced -# performance. This can be particularly helpful with template rich C++ code for -# which doxygen's built-in parser lacks the necessary type information. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. -# The default value is: NO. - -CLANG_ASSISTED_PARSING = NO - -# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS -# tag is set to YES then doxygen will add the directory of each input to the -# include path. -# The default value is: YES. -# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. - -CLANG_ADD_INC_PATHS = YES - -# If clang assisted parsing is enabled you can provide the compiler with command -# line options that you would normally use when invoking the compiler. Note that -# the include paths will already be set by doxygen for the files and directories -# specified with INPUT and INCLUDE_PATH. -# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. - -CLANG_OPTIONS = - -# If clang assisted parsing is enabled you can provide the clang parser with the -# path to the directory containing a file called compile_commands.json. This -# file is the compilation database (see: -# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the -# options used when the source files were built. This is equivalent to -# specifying the -p option to a clang tool, such as clang-check. These options -# will then be passed to the parser. Any options specified with CLANG_OPTIONS -# will be added as well. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. - -CLANG_DATABASE_PATH = - #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- diff --git a/README.md b/README.md index 354a7493c..ce25cf923 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,9 @@ The [mympd-scripts](https://github.com/jcorporation/mympd-scripts) repository pr For information on installation and configuration, see the [myMPD documentation](https://jcorporation.github.io/myMPD/) -- [Installation](https://jcorporation.github.io/myMPD/installation/) -- [Configuration](https://jcorporation.github.io/myMPD/configuration/) -- [Running](https://jcorporation.github.io/myMPD/running) +- [Installation](https://jcorporation.github.io/myMPD/010-installation/) +- [Configuration](https://jcorporation.github.io/myMPD/020-configuration/) +- [Running](https://jcorporation.github.io/myMPD/030-running/) ## Support diff --git a/contrib/lualibs/mympd/10-mympd.lua b/contrib/lualibs/mympd/10-mympd.lua index 78cad7e4d..564c66c8a 100644 --- a/contrib/lualibs/mympd/10-mympd.lua +++ b/contrib/lualibs/mympd/10-mympd.lua @@ -5,13 +5,23 @@ function mympd.init() return mympd.api("INTERNAL_API_SCRIPT_INIT") end ---- Calls the myMPD jsonrpc api --- @param method --- @param params +--- Calls the myMPD jsonrpc api for current partition +-- @param method myMPD API method +-- @param params API parameters as lua table -- @return 0 for success, else 1 -- @return jsonrpc result for success, else error function mympd.api(method, params) - local rc, raw_result = mympd_api(mympd_env.partition, method, json.encode(params)) + return mympd.api_partition(method, params, mympd_env.partition) +end + +--- Calls the myMPD jsonrpc api +-- @param partition MPD Partition +-- @param method myMPD API method +-- @param params API parameters as lua table +-- @return 0 for success, else 1 +-- @return jsonrpc result for success, else error +function mympd.api_partition(partition, method, params) + local rc, raw_result = mympd_api(partition, method, json.encode(params)) local result = json.decode(raw_result) if rc == 0 then return rc, result["result"] diff --git a/contrib/man/mympd-config.1 b/contrib/man/mympd-config.1 index f9feedb07..4fc5bd8db 100644 --- a/contrib/man/mympd-config.1 +++ b/contrib/man/mympd-config.1 @@ -1,6 +1,6 @@ .\" Manpage for mympd-config. .\" Contact to correct errors or typos. -.TH man 1 "21 Nov 2024" "18.2.2" "mympd-config man page" +.TH man 1 "15 Dec 2024" "19.0.0" "mympd-config man page" .SH NAME mympd-config \- mympd configuration tool diff --git a/contrib/man/mympd-script.1 b/contrib/man/mympd-script.1 index 2ff77bfb0..393d571be 100644 --- a/contrib/man/mympd-script.1 +++ b/contrib/man/mympd-script.1 @@ -1,6 +1,6 @@ .\" Manpage for mympd-script. .\" Contact to correct errors or typos. -.TH man 1 "21 Nov 2024" "18.2.2" "mympd-script man page" +.TH man 1 "15 Dec 2024" "19.0.0" "mympd-script man page" .SH NAME mympd-script \- mympd command line tool to execute scripts diff --git a/contrib/man/mympd.1 b/contrib/man/mympd.1 index f29587c4f..befc2ffb2 100644 --- a/contrib/man/mympd.1 +++ b/contrib/man/mympd.1 @@ -1,6 +1,6 @@ .\" Manpage for mympd. .\" Contact to correct errors or typos. -.TH man 1 "21 Nov 2024" "18.2.2" "mympd man page" +.TH man 1 "15 Dec 2024" "19.0.0" "mympd man page" .SH NAME myMPD \- standalone and mobile friendly web mpd client diff --git a/contrib/packaging/alpine/APKBUILD b/contrib/packaging/alpine/APKBUILD index 59be517fd..3655f95bc 100644 --- a/contrib/packaging/alpine/APKBUILD +++ b/contrib/packaging/alpine/APKBUILD @@ -6,14 +6,26 @@ # Maintainer: Juergen Mang # pkgname=mympd -pkgver=18.2.2 +pkgver=19.0.0 pkgrel=0 pkgdesc="myMPD is a standalone and mobile friendly web-based MPD client." url="https://jcorporation.github.io/myMPD/" arch="all" license="GPL-3.0-or-later" -depends="libid3tag flac openssl lua5.4 pcre2 newt" -makedepends="cmake perl gzip jq libid3tag-dev flac-dev openssl-dev linux-headers lua5.4-dev pcre2-dev" +depends="newt" +makedepends=" + cmake + flac-dev + gzip + jq + lua5.4 + lua5.4-dev + libid3tag-dev + linux-headers + openssl-dev + pcre2-dev + perl + " install="$pkgname.pre-install" subpackages="$pkgname-dbg $pkgname-doc" source="mympd_$pkgver.orig.tar.gz" @@ -22,10 +34,10 @@ options="!check" #no test suite build() { - cmake -B "$builddir/release" -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo "$builddir" - make -C "$builddir/release" + cmake -B "$builddir/release" -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo "$builddir" + make -C "$builddir/release" } package() { - make -C "$builddir/release" DESTDIR="$pkgdir" install + make -C "$builddir/release" DESTDIR="$pkgdir" install } diff --git a/contrib/packaging/alpine/APKBUILD.in b/contrib/packaging/alpine/APKBUILD.in index b824f8bfd..0fadf62a9 100644 --- a/contrib/packaging/alpine/APKBUILD.in +++ b/contrib/packaging/alpine/APKBUILD.in @@ -12,8 +12,20 @@ pkgdesc="myMPD is a standalone and mobile friendly web-based MPD client." url="https://jcorporation.github.io/myMPD/" arch="all" license="GPL-3.0-or-later" -depends="libid3tag flac openssl lua5.4 pcre2 newt" -makedepends="cmake perl gzip jq libid3tag-dev flac-dev openssl-dev linux-headers lua5.4-dev pcre2-dev" +depends="newt" +makedepends=" + cmake + flac-dev + gzip + jq + lua5.4 + lua5.4-dev + libid3tag-dev + linux-headers + openssl-dev + pcre2-dev + perl + " install="$pkgname.pre-install" subpackages="$pkgname-dbg $pkgname-doc" source="mympd_$pkgver.orig.tar.gz" @@ -22,10 +34,10 @@ options="!check" #no test suite build() { - cmake -B "$builddir/release" -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo "$builddir" - make -C "$builddir/release" + cmake -B "$builddir/release" -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo "$builddir" + make -C "$builddir/release" } package() { - make -C "$builddir/release" DESTDIR="$pkgdir" install + make -C "$builddir/release" DESTDIR="$pkgdir" install } diff --git a/contrib/packaging/arch/PKGBUILD b/contrib/packaging/arch/PKGBUILD index ab96c82cd..88cd3018c 100644 --- a/contrib/packaging/arch/PKGBUILD +++ b/contrib/packaging/arch/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Juergen Mang pkgname=mympd -pkgver=18.2.2 +pkgver=19.0.0 pkgrel=1 pkgdesc="A standalone and mobile friendly web-based MPD client." arch=('i686' 'x86_64' 'armv6h' 'armv7h' 'aarch64') diff --git a/contrib/packaging/debian/changelog b/contrib/packaging/debian/changelog index 90b94c277..f06ecc130 100644 --- a/contrib/packaging/debian/changelog +++ b/contrib/packaging/debian/changelog @@ -1,5 +1,5 @@ -mympd (18.2.2-1) unstable; urgency=medium +mympd (19.0.0-1) unstable; urgency=medium * Release from master - -- Juergen Mang Thu, 21 Nov 2024 18:11:46 +0100 + -- Juergen Mang Sun, 15 Dec 2024 14:12:40 +0100 diff --git a/contrib/packaging/freebsd/multimedia/mympd/Makefile b/contrib/packaging/freebsd/multimedia/mympd/Makefile index bcb06bb49..3c46377c6 100644 --- a/contrib/packaging/freebsd/multimedia/mympd/Makefile +++ b/contrib/packaging/freebsd/multimedia/mympd/Makefile @@ -1,6 +1,6 @@ PORTNAME= myMPD DISTVERSIONPREFIX= v -DISTVERSION= 18.2.2 +DISTVERSION= 19.0.0 CATEGORIES= multimedia MAINTAINER= robert.david@posteo.net diff --git a/contrib/packaging/gentoo/media-sound/mympd/mympd-18.2.2.ebuild b/contrib/packaging/gentoo/media-sound/mympd/mympd-19.0.0.ebuild similarity index 100% rename from contrib/packaging/gentoo/media-sound/mympd/mympd-18.2.2.ebuild rename to contrib/packaging/gentoo/media-sound/mympd/mympd-19.0.0.ebuild diff --git a/contrib/packaging/openwrt/Makefile b/contrib/packaging/openwrt/Makefile index ffd84032f..6c3824e8d 100644 --- a/contrib/packaging/openwrt/Makefile +++ b/contrib/packaging/openwrt/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME := mympd -PKG_VERSION := 18.2.2 +PKG_VERSION := 19.0.0 PKG_RELEASE := 1 PKG_SOURCE := $(PKG_NAME)-$(PKG_VERSION).tar.gz diff --git a/contrib/packaging/rpm/mympd.spec b/contrib/packaging/rpm/mympd.spec index afa564d43..4a8a77c08 100644 --- a/contrib/packaging/rpm/mympd.spec +++ b/contrib/packaging/rpm/mympd.spec @@ -4,7 +4,7 @@ # (c) 2018-2024 Juergen Mang Name: mympd -Version: 18.2.2 +Version: 19.0.0 Release: 0 License: GPL-3.0-or-later Group: Productivity/Multimedia/Sound/Players @@ -24,7 +24,7 @@ BuildRequires: pkgconfig BuildRequires: unzip BuildRequires: gzip BuildRequires: jq -Requires: whiptail +Requires: newt BuildRoot: %{_tmppath}/%{name}-%{version}-build %description @@ -65,5 +65,5 @@ fi %license LICENSE.md %changelog -* Thu Nov 21 2024 Juergen Mang 18.2.2-0 +* Sun Dec 15 2024 Juergen Mang 19.0.0-0 - Version from master diff --git a/contrib/packaging/rpm/mympd.spec.in b/contrib/packaging/rpm/mympd.spec.in index 0fa6cb629..c442ecab3 100644 --- a/contrib/packaging/rpm/mympd.spec.in +++ b/contrib/packaging/rpm/mympd.spec.in @@ -24,7 +24,7 @@ BuildRequires: pkgconfig BuildRequires: unzip BuildRequires: gzip BuildRequires: jq -Requires: whiptail +Requires: newt BuildRoot: %{_tmppath}/%{name}-%{version}-build %description diff --git a/dist/bootstrap-native/bootstrap-native.js b/dist/bootstrap-native/bootstrap-native.js index dabff4282..1c388d548 100644 --- a/dist/bootstrap-native/bootstrap-native.js +++ b/dist/bootstrap-native/bootstrap-native.js @@ -1,352 +1,240 @@ var BSN = function(exports) { - "use strict";var __defProp = Object.defineProperty; -var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - - const e = {}, f = (t) => { - const { type: c, currentTarget: i2 } = t; - [...e[c]].forEach(([n, s]) => { - /* istanbul ignore else @preserve */ - i2 === n && [...s].forEach(([o, a]) => { - o.apply(n, [t]), typeof a == "object" && a.once && r(n, c, o, a); - }); - }); - }, E$1 = (t, c, i2, n) => { - /* istanbul ignore else @preserve */ - e[c] || (e[c] = /* @__PURE__ */ new Map()); - const s = e[c]; - /* istanbul ignore else @preserve */ - s.has(t) || s.set(t, /* @__PURE__ */ new Map()); - const o = s.get(t), { size: a } = o; - o.set(i2, n); - /* istanbul ignore else @preserve */ - a || t.addEventListener(c, f, n); - }, r = (t, c, i2, n) => { - const s = e[c], o = s && s.get(t), a = o && o.get(i2), d2 = a !== void 0 ? a : n; - /* istanbul ignore else @preserve */ - o && o.has(i2) && o.delete(i2); - /* istanbul ignore else @preserve */ - s && (!o || !o.size) && s.delete(t); - /* istanbul ignore else @preserve */ - (!s || !s.size) && delete e[c]; - /* istanbul ignore else @preserve */ - (!o || !o.size) && t.removeEventListener( - c, - f, - d2 - ); - }, g$1 = E$1, M$1 = r; - const eventListener = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - addListener: E$1, - globalListener: f, - off: M$1, - on: g$1, - registry: e, - removeListener: r - }, Symbol.toStringTag, { value: "Module" })); - const we = "aria-describedby", Ae = "aria-expanded", X = "aria-hidden", Te = "aria-modal", ke = "aria-pressed", De = "aria-selected", P = "DOMContentLoaded", ot = "focus", st = "focusin", ct = "focusout", ut = "keydown", dt = "keyup", ft = "click", gt = "mousedown", Et = "hover", bt = "mouseenter", ht = "mouseleave", Dt = "pointerdown", Ot = "pointermove", Lt = "pointerup", zt = "resize", Ht = "scroll", Ut = "touchstart", Ve = "dragstart", qt = 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]', Je = "ArrowDown", Xe = "ArrowUp", Ye = "ArrowLeft", Ze = "ArrowRight", sn = "Escape", Jt = "transitionDuration", Xt = "transitionDelay", C = "transitionend", W = "transitionProperty", Yt = navigator.userAgentData, A = Yt, { userAgent: Zt } = navigator, S = Zt, z = /iPhone|iPad|iPod|Android/i; - // istanbul ignore else @preserve - A ? A.brands.some((t) => z.test(t.brand)) : z.test(S); - const V = /(iPhone|iPod|iPad)/, An = A ? A.brands.some((t) => V.test(t.brand)) : ( - /* istanbul ignore next @preserve */ - V.test(S) - ); - S ? S.includes("Firefox") : ( - /* istanbul ignore next @preserve */ - false - ); - const { head: N } = document; - ["webkitPerspective", "perspective"].some((t) => t in N.style); - const R = (t, e2, n, o) => { + "use strict"; + const Me = "aria-describedby", De = "aria-expanded", $ = "aria-hidden", ze = "aria-modal", Ie = "aria-pressed", Pe = "aria-selected", st = "focus", rt = "focusin", ct = "focusout", lt = "keydown", ft = "keyup", gt = "click", vt = "mousedown", Et = "hover", ht = "mouseenter", yt = "mouseleave", Dt = "pointerdown", Ot = "pointermove", xt = "pointerup", Wt = "touchstart", Re = "dragstart", qt = 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]', en = "ArrowDown", nn = "ArrowUp", on = "ArrowLeft", sn = "ArrowRight", fn = "Escape", _t = "transitionDuration", $t = "transitionDelay", M = "transitionend", W = "transitionProperty", On = () => { + const t = /(iPhone|iPod|iPad)/; + return navigator?.userAgentData?.brands.some( + (e2) => t.test(e2.brand) + ) || t.test( + navigator?.userAgent + ) || false; + }, Yt = () => { + }, R = (t, e2, n, o) => { const s = o || false; - t.addEventListener(e2, n, s); + t.addEventListener( + e2, + n, + s + ); }, Q = (t, e2, n, o) => { const s = o || false; - t.removeEventListener(e2, n, s); - }, $t = (t, e2, n, o) => { - const s = (c) => { - /* istanbul ignore else @preserve */ - (c.target === t || c.currentTarget === t) && (n.apply(t, [c]), Q(t, e2, s, o)); - }; - R(t, e2, s, o); - }, _t = () => { - }; - (() => { - let t = false; - try { - const e2 = Object.defineProperty({}, "passive", { - get: () => (t = true, t) - }); - // istanbul ignore next @preserve - $t(document, P, _t, e2); - } catch { - } - return t; - })(); - ["webkitTransform", "transform"].some((t) => t in N.style); - ["webkitAnimation", "animation"].some((t) => t in N.style); - ["webkitTransition", "transition"].some((t) => t in N.style); - const j = (t, e2) => t.getAttribute(e2), te = (t, e2) => t.hasAttribute(e2), In = (t, e2, n) => t.setAttribute(e2, n), zn = (t, e2) => t.removeAttribute(e2), Bn = (t, ...e2) => { + t.removeEventListener( + e2, + n, + s + ); + }, j = (t, e2) => t.getAttribute(e2), ee = (t, e2) => t.hasAttribute(e2), Wn = (t, e2, n) => t.setAttribute(e2, n), Qn = (t, e2) => t.removeAttribute(e2), Kn = (t, ...e2) => { t.classList.add(...e2); - }, Fn = (t, ...e2) => { + }, qn = (t, ...e2) => { t.classList.remove(...e2); - }, Hn = (t, e2) => t.classList.contains(e2), v = (t) => t != null && typeof t == "object" || false, i = (t) => v(t) && typeof t.nodeType == "number" && [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].some((e2) => t.nodeType === e2) || false, l = (t) => i(t) && t.nodeType === 1 || false, E = /* @__PURE__ */ new Map(), L = { - data: E, - /** - * Sets web components data. - * - * @param element target element - * @param component the component's name or a unique key - * @param instance the component instance - */ + }, Gn = (t, e2) => t.classList.contains(e2), v$1 = (t) => t != null && typeof t == "object" || false, u = (t) => v$1(t) && typeof t.nodeType == "number" && [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].some( + (e2) => t.nodeType === e2 + ) || false, i = (t) => u(t) && t.nodeType === 1 || false, E$1 = /* @__PURE__ */ new Map(), L = { + data: E$1, set: (t, e2, n) => { - if (!l(t)) return; - // istanbul ignore else @preserve - E.has(e2) || E.set(e2, /* @__PURE__ */ new Map()), E.get(e2).set(t, n); + if (!i(t)) return; + E$1.has(e2) || E$1.set(e2, /* @__PURE__ */ new Map()), E$1.get(e2).set(t, n); }, - /** - * Returns all instances for specified component. - * - * @param component the component's name or a unique key - * @returns all the component instances - */ - getAllFor: (t) => E.get(t) || null, - /** - * Returns the instance associated with the target. - * - * @param element target element - * @param component the component's name or a unique key - * @returns the instance - */ + getAllFor: (t) => E$1.get(t) || null, get: (t, e2) => { - if (!l(t) || !e2) return null; + if (!i(t) || !e2) return null; const n = L.getAllFor(e2); return t && n && n.get(t) || null; }, - /** - * Removes web components data. - * - * @param element target element - * @param component the component's name or a unique key - */ remove: (t, e2) => { const n = L.getAllFor(e2); - if (!n || !l(t)) return; - n.delete(t); - // istanbul ignore else @preserve - n.size === 0 && E.delete(e2); + !n || !i(t) || (n.delete(t), n.size === 0 && E$1.delete(e2)); } - }, Rn = (t, e2) => L.get(t, e2), M = (t) => typeof t == "string" || false, q = (t) => v(t) && t.constructor.name === "Window" || false, G = (t) => i(t) && t.nodeType === 9 || false, d = (t) => q(t) ? t.document : G(t) ? t : i(t) ? t.ownerDocument : window.document, T = (t, ...e2) => Object.assign(t, ...e2), ee = (t) => { + }, Xn = (t, e2) => L.get(t, e2), I = (t) => t?.trim().replace( + /(?:^\w|[A-Z]|\b\w)/g, + (e2, n) => n === 0 ? e2.toLowerCase() : e2.toUpperCase() + ).replace(/\s+/g, ""), N = (t) => typeof t == "string" || false, K = (t) => v$1(t) && t.constructor.name === "Window" || false, q = (t) => u(t) && t.nodeType === 9 || false, d = (t) => q(t) ? t : u(t) ? t.ownerDocument : K(t) ? t.document : globalThis.document, C = (t, ...e2) => Object.assign(t, ...e2), ne = (t) => { if (!t) return; - if (M(t)) + if (N(t)) return d().createElement(t); - const { tagName: e2 } = t, n = ee(e2); + const { tagName: e2 } = t, n = ne(e2); if (!n) return; const o = { ...t }; - return delete o.tagName, T(n, o); - }, K = (t, e2) => t.dispatchEvent(e2), g = (t, e2) => { - const n = getComputedStyle(t), o = e2.replace("webkit", "Webkit").replace(/([A-Z])/g, "-$1").toLowerCase(); - return n.getPropertyValue(o); + return delete o.tagName, C(n, o); + }, G = (t, e2) => t.dispatchEvent(e2), f$1 = (t, e2, n) => { + const o = getComputedStyle(t, n), s = e2.replace("webkit", "Webkit").replace(/([A-Z])/g, "-$1").toLowerCase(); + return o.getPropertyValue(s); }, ce = (t) => { - const e2 = g(t, W), n = g(t, Xt), o = n.includes("ms") ? ( - /* istanbul ignore next */ - 1 - ) : 1e3, s = e2 && e2 !== "none" ? parseFloat(n) * o : ( - /* istanbul ignore next */ - 0 - ); - return Number.isNaN(s) ? ( - /* istanbul ignore next */ - 0 - ) : s; - }, re = (t) => { - const e2 = g(t, W), n = g(t, Jt), o = n.includes("ms") ? ( - /* istanbul ignore next */ - 1 - ) : 1e3, s = e2 && e2 !== "none" ? parseFloat(n) * o : ( - /* istanbul ignore next */ - 0 - ); - return Number.isNaN(s) ? ( - /* istanbul ignore next */ - 0 - ) : s; - }, qn = (t, e2) => { + const e2 = f$1(t, W), n = f$1(t, $t), o = n.includes("ms") ? 1 : 1e3, s = e2 && e2 !== "none" ? parseFloat(n) * o : 0; + return Number.isNaN(s) ? 0 : s; + }, ae = (t) => { + const e2 = f$1(t, W), n = f$1(t, _t), o = n.includes("ms") ? 1 : 1e3, s = e2 && e2 !== "none" ? parseFloat(n) * o : 0; + return Number.isNaN(s) ? 0 : s; + }, no = (t, e2) => { let n = 0; - const o = new Event(C), s = re(t), c = ce(t); + const o = new Event(M), s = ae(t), r2 = ce(t); if (s) { - const a = (u) => { - // istanbul ignore else @preserve - u.target === t && (e2.apply(t, [u]), t.removeEventListener(C, a), n = 1); + const a2 = (l) => { + l.target === t && (e2.apply(t, [l]), t.removeEventListener(M, a2), n = 1); }; - t.addEventListener(C, a), setTimeout(() => { - // istanbul ignore next @preserve - n || K(t, o); - }, s + c + 17); + t.addEventListener(M, a2), setTimeout(() => { + n || G(t, o); + }, s + r2 + 17); } else e2.apply(t, [o]); - }, Jn = (t, e2) => t.focus(e2), B = (t) => ["true", true].includes(t) ? true : ["false", false].includes(t) ? false : ["null", "", null, void 0].includes(t) ? null : t !== "" && !Number.isNaN(+t) ? +t : t, w = (t) => Object.entries(t), ae = (t) => t.toLowerCase(), Xn = (t, e2, n, o) => { - const s = { ...n }, c = { ...t.dataset }, a = { ...e2 }, u = {}, p = "title"; - return w(c).forEach(([r2, f2]) => { - const y = typeof r2 == "string" && r2.includes(o) ? r2.replace(o, "").replace(/[A-Z]/g, (J) => ae(J)) : ( - /* istanbul ignore next @preserve */ - r2 - ); - u[y] = B(f2); - }), w(s).forEach(([r2, f2]) => { - s[r2] = B(f2); - }), w(e2).forEach(([r2, f2]) => { - // istanbul ignore else @preserve - r2 in s ? a[r2] = s[r2] : r2 in u ? a[r2] = u[r2] : a[r2] = r2 === p ? j(t, p) : f2; - }), a; - }, Zn = (t) => Object.keys(t), $n = (t) => Object.values(t), to = (t, e2) => { + }, ro = (t, e2) => t.focus(e2), P = (t) => ["true", true].includes(t) ? true : ["false", false].includes(t) ? false : ["null", "", null, void 0].includes(t) ? null : t !== "" && !Number.isNaN(+t) ? +t : t, S = (t) => Object.entries(t), ao = (t, e2, n, o) => { + if (!i(t)) return e2; + const s = { ...n }, r2 = { ...t.dataset }, a2 = { ...e2 }, l = {}, p2 = "title"; + return S(r2).forEach(([c, g]) => { + const A = typeof c == "string" && c.includes(o) ? I(c.replace(o, "")) : I(c); + l[A] = P(g); + }), S(s).forEach(([c, g]) => { + s[c] = P(g); + }), S(e2).forEach(([c, g]) => { + c in s ? a2[c] = s[c] : c in l ? a2[c] = l[c] : a2[c] = c === p2 ? j(t, p2) : g; + }), a2; + }, uo = (t) => Object.keys(t), po = (t, e2) => { const n = new CustomEvent(t, { cancelable: true, bubbles: true }); - // istanbul ignore else @preserve - return v(e2) && T(n, e2), n; - }, eo = { passive: true }, no = (t) => t.offsetHeight, oo = (t, e2) => { - w(e2).forEach(([n, o]) => { - if (o && M(n) && n.includes("--")) + return v$1(e2) && C(n, e2), n; + }, go = { passive: true }, mo = (t) => t.offsetHeight, vo = (t, e2) => { + S(e2).forEach(([n, o]) => { + if (o && N(n) && n.includes("--")) t.style.setProperty(n, o); else { const s = {}; - s[n] = o, T(t.style, s); + s[n] = o, C(t.style, s); } }); - }, I = (t) => v(t) && t.constructor.name === "Map" || false, ie = (t) => typeof t == "number" || false, m = /* @__PURE__ */ new Map(), so = { - /** - * Sets a new timeout timer for an element, or element -> key association. - * - * @param element target element - * @param callback the callback - * @param delay the execution delay - * @param key a unique key - */ + }, O = (t) => v$1(t) && t.constructor.name === "Map" || false, ie = (t) => typeof t == "number" || false, m$1 = /* @__PURE__ */ new Map(), bo = { set: (t, e2, n, o) => { - if (!l(t)) return; - // istanbul ignore else @preserve - if (o && o.length) { - // istanbul ignore else @preserve - m.has(t) || m.set(t, /* @__PURE__ */ new Map()), m.get(t).set(o, setTimeout(e2, n)); - } else - m.set(t, setTimeout(e2, n)); + i(t) && (o && o.length ? (m$1.has(t) || m$1.set(t, /* @__PURE__ */ new Map()), m$1.get(t).set(o, setTimeout(e2, n))) : m$1.set(t, setTimeout(e2, n))); }, - /** - * Returns the timer associated with the target. - * - * @param element target element - * @param key a unique - * @returns the timer - */ get: (t, e2) => { - if (!l(t)) return null; - const n = m.get(t); - return e2 && n && I(n) ? n.get(e2) || /* istanbul ignore next */ - null : ie(n) ? n : null; + if (!i(t)) return null; + const n = m$1.get(t); + return e2 && n && O(n) ? n.get(e2) || null : ie(n) ? n : null; }, - /** - * Clears the element's timer. - * - * @param element target element - * @param key a unique key - */ clear: (t, e2) => { - if (!l(t)) return; - const n = m.get(t); - if (e2 && e2.length && I(n)) { - clearTimeout(n.get(e2)), n.delete(e2); - // istanbul ignore else @preserve - n.size === 0 && m.delete(t); - } else - clearTimeout(n), m.delete(t); - } - }, ue = (t, e2) => (i(e2) ? e2 : d()).querySelectorAll(t), x = /* @__PURE__ */ new Map(); + if (!i(t)) return; + const n = m$1.get(t); + e2 && e2.length && O(n) ? (clearTimeout(n.get(e2)), n.delete(e2), n.size === 0 && m$1.delete(t)) : (clearTimeout(n), m$1.delete(t)); + } + }, Eo = (t) => t.toLowerCase(), ue = (t, e2) => (u(e2) ? e2 : d()).querySelectorAll(t), x = /* @__PURE__ */ new Map(); function le(t) { - const { shiftKey: e2, code: n } = t, o = d(this), s = [...ue(qt, this)].filter( - (u) => !te(u, "disabled") && !j(u, X) + const { shiftKey: e2, code: n } = t, o = d(this), s = [ + ...ue(qt, this) + ].filter( + (l) => !ee(l, "disabled") && !j(l, $) ); if (!s.length) return; - const c = s[0], a = s[s.length - 1]; - // istanbul ignore else @preserve - n === "Tab" && (e2 && o.activeElement === c ? (a.focus(), t.preventDefault()) : !e2 && o.activeElement === a && (c.focus(), t.preventDefault())); + const r2 = s[0], a2 = s[s.length - 1]; + n === "Tab" && (e2 && o.activeElement === r2 ? (a2.focus(), t.preventDefault()) : !e2 && o.activeElement === a2 && (r2.focus(), t.preventDefault())); } - const de = (t) => x.has(t) === true, ro = (t) => { + const de = (t) => x.has(t) === true, yo = (t) => { const e2 = de(t); (e2 ? Q : R)(t, "keydown", le), e2 ? x.delete(t) : x.set(t, true); - }, h = (t, e2) => { - const { width: n, height: o, top: s, right: c, bottom: a, left: u } = t.getBoundingClientRect(); - let p = 1, r2 = 1; - if (e2 && l(t)) { - const { offsetWidth: f2, offsetHeight: y } = t; - p = f2 > 0 ? Math.round(n) / f2 : ( - /* istanbul ignore next */ - 1 - ), r2 = y > 0 ? Math.round(o) / y : ( - /* istanbul ignore next */ - 1 - ); + }, b = (t) => i(t) && "offsetWidth" in t || false, y = (t, e2) => { + const { width: n, height: o, top: s, right: r2, bottom: a2, left: l } = t.getBoundingClientRect(); + let p2 = 1, c = 1; + if (e2 && b(t)) { + const { offsetWidth: g, offsetHeight: A } = t; + p2 = g > 0 ? Math.round(n) / g : 1, c = A > 0 ? Math.round(o) / A : 1; + } + return { + width: n / p2, + height: o / c, + top: s / c, + right: r2 / p2, + bottom: a2 / c, + left: l / p2, + x: l / p2, + y: s / c + }; + }, wo = (t) => d(t).body, w$1 = (t) => d(t).documentElement, So = (t) => { + const e2 = K(t), n = e2 ? t.scrollX : t.scrollLeft, o = e2 ? t.scrollY : t.scrollTop; + return { x: n, y: o }; + }, pe = (t) => u(t) && t.constructor.name === "ShadowRoot" || false, k$1 = (t) => t.nodeName === "HTML" ? t : i(t) && t.assignedSlot || u(t) && t.parentNode || pe(t) && t.host || w$1(t), ge = (t) => t ? q(t) ? t.defaultView : u(t) ? t?.ownerDocument?.defaultView : t : window, me = (t) => u(t) && ["TABLE", "TD", "TH"].includes(t.nodeName) || false, ve = (t, e2) => t.matches(e2), he = (t) => { + if (!b(t)) return false; + const { width: e2, height: n } = y(t), { offsetWidth: o, offsetHeight: s } = t; + return Math.round(e2) !== o || Math.round(n) !== s; + }, No = (t, e2, n) => { + const o = b(e2), s = y( + t, + o && he(e2) + ), r2 = { x: 0, y: 0 }; + if (o) { + const a2 = y(e2, true); + r2.x = a2.x + e2.clientLeft, r2.y = a2.y + e2.clientTop; } return { - width: n / p, - height: o / r2, - top: s / r2, - right: c / p, - bottom: a / r2, - left: u / p, - x: u / p, - y: s / r2 + x: s.left + n.x - r2.x, + y: s.top + n.y - r2.y, + width: s.width, + height: s.height }; - }, ao = (t) => d(t).body, k = (t) => d(t).documentElement, pe = (t) => i(t) && t.constructor.name === "ShadowRoot" || false, lo = (t) => t.nodeName === "HTML" ? t : l(t) && t.assignedSlot || // step into the shadow DOM of the parent of a slotted node - i(t) && t.parentNode || // DOM Element detected - pe(t) && t.host || // ShadowRoot detected - k(t); - let F = 0, H = 0; - const b = /* @__PURE__ */ new Map(), me = (t, e2) => { - let n = e2 ? F : H; + }; + let B = 0, V = 0; + const h$1 = /* @__PURE__ */ new Map(), ye = (t, e2) => { + let n = e2 ? B : V; if (e2) { - const o = me(t), s = b.get(o) || /* @__PURE__ */ new Map(); - b.has(o) || b.set(o, s), I(s) && !s.has(e2) ? (s.set(e2, n), F += 1) : n = s.get(e2); + const o = ye(t), s = h$1.get(o) || /* @__PURE__ */ new Map(); + h$1.has(o) || h$1.set(o, s), O(s) && !s.has(e2) ? (s.set(e2, n), B += 1) : n = s.get(e2); } else { const o = t.id || t; - b.has(o) ? n = b.get(o) : (b.set(o, n), H += 1); + h$1.has(o) ? n = h$1.get(o) : (h$1.set(o, n), V += 1); } return n; - }, fo = (t) => { - var e2; - return t ? G(t) ? t.defaultView : i(t) ? (e2 = t == null ? void 0 : t.ownerDocument) == null ? void 0 : e2.defaultView : t : window; - }, ge = (t) => Array.isArray(t) || false, vo = (t) => { - if (!i(t)) return false; - const { top: e2, bottom: n } = h(t), { clientHeight: o } = k(t); + }, we = (t) => Array.isArray(t) || false, To = (t) => { + if (!u(t)) return false; + const { top: e2, bottom: n } = y(t), { clientHeight: o } = w$1(t); return e2 <= o && n >= 0; - }, ho = (t) => typeof t == "function" || false, Mo = (t) => v(t) && t.constructor.name === "NodeList" || false, To = (t) => k(t).dir === "rtl", Do = (t) => i(t) && ["TABLE", "TD", "TH"].includes(t.nodeName) || false, Ee = (t, e2) => t ? t.closest(e2) || // break out of `ShadowRoot` - Ee(t.getRootNode().host, e2) : null, Co = (t, e2) => l(t) ? t : (i(e2) ? e2 : d()).querySelector(t), be = (t, e2) => (i(e2) ? e2 : d()).getElementsByTagName(t), Io = (t, e2) => (e2 && i(e2) ? e2 : d()).getElementsByClassName(t), xo = (t, e2) => t.matches(e2); + }, Lo = (t) => typeof t == "function" || false, Fo = (t) => v$1(t) && t.constructor.name === "NodeList" || false, Bo = (t) => w$1(t).dir === "rtl", Se = (t, e2) => !t || !e2 ? null : t.closest(e2) || Se(t.getRootNode().host, e2) || null, Ho = (t, e2) => i(t) ? t : (i(e2) ? e2 : d()).querySelector(t), ke = (t, e2) => (u(e2) ? e2 : d()).getElementsByTagName( + t + ), Ro = (t, e2) => (e2 && u(e2) ? e2 : d()).getElementsByClassName( + t + ); + const e = {}, f = (t) => { + const { type: n, currentTarget: c } = t; + e[n].forEach((a2, s) => { + c === s && a2.forEach((o, i2) => { + i2.apply(s, [t]), typeof o == "object" && o.once && r(s, n, i2, o); + }); + }); + }, E = (t, n, c, a2) => { + e[n] || (e[n] = /* @__PURE__ */ new Map()); + const s = e[n]; + s.has(t) || s.set(t, /* @__PURE__ */ new Map()); + const o = s.get( + t + ), { size: i2 } = o; + o.set(c, a2), i2 || t.addEventListener( + n, + f, + a2 + ); + }, r = (t, n, c, a2) => { + const s = e[n], o = s && s.get(t), i2 = o && o.get(c), d2 = i2 !== void 0 ? i2 : a2; + o && o.has(c) && o.delete(c), s && (!o || !o.size) && s.delete(t), (!s || !s.size) && delete e[n], (!o || !o.size) && t.removeEventListener( + n, + f, + d2 + ); + }; const fadeClass = "fade"; const showClass = "show"; const dataBsDismiss = "data-bs-dismiss"; const alertString = "alert"; const alertComponent = "Alert"; - const version = "5.0.15"; + const isDisabled = (target) => { + return Gn(target, "disabled") || j(target, "disabled") === "true"; + }; + const version = "5.1.0"; const Version = version; class BaseComponent { - /** - * @param target `HTMLElement` or selector string - * @param config component instance options - */ constructor(target, config) { - /** just to have something to extend from */ - // istanbul ignore next @preserve coverage wise this isn't important - __publicField(this, "_toggleEventListeners", () => { - }); let element; try { - if (l(target)) { + if (i(target)) { element = target; - } else if (M(target)) { - element = Co(target); - // istanbul ignore else @preserve + } else if (N(target)) { + element = Ho(target); if (!element) throw Error(`"${target}" is not a valid selector.`); } else { throw Error(`your target is not an instance of HTMLElement.`); @@ -355,164 +243,126 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy throw Error(`${this.name} Error: ${e2.message}`); } const prevInstance = L.get(element, this.name); - // istanbul ignore else @preserve if (prevInstance) { prevInstance._toggleEventListeners(); } this.element = element; - this.options = this.defaults && Zn(this.defaults).length ? Xn(element, this.defaults, config || {}, "bs") : {}; + this.options = this.defaults && uo(this.defaults).length ? ao(element, this.defaults, config || {}, "bs") : {}; L.set(element, this.name, this); } - // istanbul ignore next @preserve get version() { return Version; } - // istanbul ignore next @preserve get name() { return "BaseComponent"; } - // istanbul ignore next @preserve get defaults() { return {}; } - /** Removes component from target element. */ + _toggleEventListeners = () => { + }; dispose() { L.remove(this.element, this.name); - Zn(this).forEach((prop) => { + uo(this).forEach((prop) => { delete this[prop]; }); } } const alertSelector = `.${alertString}`; const alertDismissSelector = `[${dataBsDismiss}="${alertString}"]`; - const getAlertInstance = (element) => Rn(element, alertComponent); + const getAlertInstance = (element) => Xn(element, alertComponent); const alertInitCallback = (element) => new Alert(element); - const closeAlertEvent = to( + const closeAlertEvent = po( `close.bs.${alertString}` ); - const closedAlertEvent = to( + const closedAlertEvent = po( `closed.bs.${alertString}` ); const alertTransitionEnd = (self) => { const { element } = self; - K(element, closedAlertEvent); + G(element, closedAlertEvent); self._toggleEventListeners(); self.dispose(); element.remove(); }; class Alert extends BaseComponent { + static selector = alertSelector; + static init = alertInitCallback; + static getInstance = getAlertInstance; + dismiss; constructor(target) { super(target); - __publicField(this, "dismiss"); - // ALERT PUBLIC METHODS - // ==================== - /** - * Public method that hides the `.alert` element from the user, - * disposes the instance once animation is complete, then - * removes the element from the DOM. - */ - __publicField(this, "close", () => { - const { element } = this; - // istanbul ignore else @preserve - if (element && Hn(element, showClass)) { - K(element, closeAlertEvent); - if (!closeAlertEvent.defaultPrevented) { - Fn(element, showClass); - if (Hn(element, fadeClass)) { - qn(element, () => alertTransitionEnd(this)); - } else alertTransitionEnd(this); - } - } - }); - /** - * Toggle on / off the `click` event listener. - * - * @param add when `true`, event listener is added - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - const { dismiss, close } = this; - // istanbul ignore else @preserve - if (dismiss) action(dismiss, ft, close); - }); - this.dismiss = Co(alertDismissSelector, this.element); + this.dismiss = Ho( + alertDismissSelector, + this.element + ); this._toggleEventListeners(true); } - /** Returns component name string. */ get name() { return alertComponent; } - /** Remove the component from target element. */ + close = (e2) => { + const { element, dismiss } = this; + if (!element || !Gn(element, showClass)) return; + if (e2 && dismiss && isDisabled(dismiss)) return; + G(element, closeAlertEvent); + if (closeAlertEvent.defaultPrevented) return; + qn(element, showClass); + if (Gn(element, fadeClass)) { + no(element, () => alertTransitionEnd(this)); + } else alertTransitionEnd(this); + }; + _toggleEventListeners = (add) => { + const action = add ? E : r; + const { dismiss, close } = this; + if (dismiss) { + action(dismiss, gt, close); + } + }; dispose() { this._toggleEventListeners(); super.dispose(); } } - __publicField(Alert, "selector", alertSelector); - __publicField(Alert, "init", alertInitCallback); - __publicField(Alert, "getInstance", getAlertInstance); const activeClass = "active"; const dataBsToggle = "data-bs-toggle"; const buttonString = "button"; const buttonComponent = "Button"; const buttonSelector = `[${dataBsToggle}="${buttonString}"]`; - const getButtonInstance = (element) => Rn(element, buttonComponent); + const getButtonInstance = (element) => Xn(element, buttonComponent); const buttonInitCallback = (element) => new Button(element); class Button extends BaseComponent { - /** - * @param target usually a `.btn` element - */ + static selector = buttonSelector; + static init = buttonInitCallback; + static getInstance = getButtonInstance; constructor(target) { super(target); - __publicField(this, "isActive", false); - // BUTTON PUBLIC METHODS - // ===================== - /** - * Toggles the state of the target button. - * - * @param e usually `click` Event object - */ - __publicField(this, "toggle", (e2) => { - if (e2) e2.preventDefault(); - const { element, isActive } = this; - if (!Hn(element, "disabled") && !j(element, "disabled")) { - const action = isActive ? Fn : Bn; - action(element, activeClass); - In(element, ke, isActive ? "false" : "true"); - this.isActive = Hn(element, activeClass); - } - }); - // BUTTON PRIVATE METHOD - // ===================== - /** - * Toggles on/off the `click` event listener. - * - * @param add when `true`, event listener is added - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - action(this.element, ft, this.toggle); - }); const { element } = this; - this.isActive = Hn(element, activeClass); - In(element, ke, String(!!this.isActive)); + this.isActive = Gn(element, activeClass); + Wn(element, Ie, String(!!this.isActive)); this._toggleEventListeners(true); } - /** - * Returns component name string. - */ get name() { return buttonComponent; } - /** Removes the `Button` component from the target element. */ + toggle = (e2) => { + if (e2) e2.preventDefault(); + const { element, isActive } = this; + if (isDisabled(element)) return; + const action = isActive ? qn : Kn; + action(element, activeClass); + Wn(element, Ie, isActive ? "false" : "true"); + this.isActive = Gn(element, activeClass); + }; + _toggleEventListeners = (add) => { + const action = add ? E : r; + action(this.element, gt, this.toggle); + }; dispose() { this._toggleEventListeners(); super.dispose(); } } - __publicField(Button, "selector", buttonSelector); - __publicField(Button, "init", buttonInitCallback); - __publicField(Button, "getInstance", getButtonInstance); const dataBsTarget = "data-bs-target"; const carouselString = "carousel"; const carouselComponent = "Carousel"; @@ -524,7 +374,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return targetAttr.map((att) => { const attValue = j(element, att); if (attValue) { - return att === dataBsParent ? Ee(element, attValue) : Co(attValue, doc); + return att === dataBsParent ? Se(element, attValue) : Ho(attValue, doc); } return null; }).filter((x2) => x2)[0]; @@ -540,27 +390,26 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy touch: true, interval: 5e3 }; - const getCarouselInstance = (element) => Rn(element, carouselComponent); + const getCarouselInstance = (element) => Xn(element, carouselComponent); const carouselInitCallback = (element) => new Carousel(element); let startX = 0; let currentX = 0; let endX = 0; - const carouselSlideEvent = to(`slide.bs.${carouselString}`); - const carouselSlidEvent = to(`slid.bs.${carouselString}`); + const carouselSlideEvent = po(`slide.bs.${carouselString}`); + const carouselSlidEvent = po(`slid.bs.${carouselString}`); const carouselTransitionEndHandler = (self) => { const { index, direction, element, slides, options } = self; - // istanbul ignore else @preserve if (self.isAnimating) { const activeItem = getActiveIndex(self); const orientation = direction === "left" ? "next" : "prev"; const directionClass = direction === "left" ? "start" : "end"; - Bn(slides[index], activeClass); - Fn(slides[index], `${carouselItem}-${orientation}`); - Fn(slides[index], `${carouselItem}-${directionClass}`); - Fn(slides[activeItem], activeClass); - Fn(slides[activeItem], `${carouselItem}-${directionClass}`); - K(element, carouselSlidEvent); - so.clear(element, dataBsSlide); + Kn(slides[index], activeClass); + qn(slides[index], `${carouselItem}-${orientation}`); + qn(slides[index], `${carouselItem}-${directionClass}`); + qn(slides[activeItem], activeClass); + qn(slides[activeItem], `${carouselItem}-${directionClass}`); + G(element, carouselSlidEvent); + bo.clear(element, dataBsSlide); if (self.cycle && !d(element).hidden && options.interval && !self.isPaused) { self.cycle(); } @@ -568,68 +417,54 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; function carouselPauseHandler() { const self = getCarouselInstance(this); - // istanbul ignore else @preserve - if (self && !self.isPaused && !so.get(this, pausedClass)) { - Bn(this, pausedClass); + if (self && !self.isPaused && !bo.get(this, pausedClass)) { + Kn(this, pausedClass); } } function carouselResumeHandler() { const self = getCarouselInstance(this); - // istanbul ignore else @preserve - if (self && self.isPaused && !so.get(this, pausedClass)) { + if (self && self.isPaused && !bo.get(this, pausedClass)) { self.cycle(); } } function carouselIndicatorHandler(e2) { e2.preventDefault(); - const element = Ee(this, carouselSelector) || getTargetElement(this); - const self = getCarouselInstance(element); - // istanbul ignore else @preserve - if (self && !self.isAnimating) { - const newIndex = +(j(this, dataBsSlideTo) || // istanbul ignore next @preserve - 0); - // istanbul ignore else @preserve - if (this && !Hn(this, activeClass) && // event target is not active - !Number.isNaN(newIndex)) { - self.to(newIndex); - } + const element = Se(this, carouselSelector) || getTargetElement(this); + const self = element && getCarouselInstance(element); + if (isDisabled(this)) return; + if (!self || self.isAnimating) return; + const newIndex = +(j(this, dataBsSlideTo) || 0); + if (this && !Gn(this, activeClass) && !Number.isNaN(newIndex)) { + self.to(newIndex); } } function carouselControlsHandler(e2) { e2.preventDefault(); - const element = Ee(this, carouselSelector) || getTargetElement(this); - const self = getCarouselInstance(element); - // istanbul ignore else @preserve - if (self && !self.isAnimating) { - const orientation = j(this, dataBsSlide); - // istanbul ignore else @preserve - if (orientation === "next") { - self.next(); - } else if (orientation === "prev") { - self.prev(); - } + const element = Se(this, carouselSelector) || getTargetElement(this); + const self = element && getCarouselInstance(element); + if (isDisabled(this)) return; + if (!self || self.isAnimating) return; + const orientation = j(this, dataBsSlide); + if (orientation === "next") { + self.next(); + } else if (orientation === "prev") { + self.prev(); } } const carouselKeyHandler = ({ code, target }) => { const doc = d(target); - const [element] = [...ue(carouselSelector, doc)].filter( - (x2) => vo(x2) - ); + const [element] = [...ue(carouselSelector, doc)].filter((x2) => To(x2)); const self = getCarouselInstance(element); - // istanbul ignore next @preserve - if (self && !self.isAnimating && !/textarea|input/i.test(target.nodeName)) { - const RTL = To(element); - const arrowKeyNext = !RTL ? Ze : Ye; - const arrowKeyPrev = !RTL ? Ye : Ze; - // istanbul ignore else @preserve - if (code === arrowKeyPrev) self.prev(); - else if (code === arrowKeyNext) self.next(); - } + if (!self || self.isAnimating || /textarea|input|select/i.test(target.nodeName)) return; + const RTL = Bo(element); + const arrowKeyNext = !RTL ? sn : on; + const arrowKeyPrev = !RTL ? on : sn; + if (code === arrowKeyPrev) self.prev(); + else if (code === arrowKeyNext) self.next(); }; function carouselDragHandler(e2) { const { target } = e2; const self = getCarouselInstance(this); - // istanbul ignore next @preserve if (self && self.isTouch && (self.indicator && !self.indicator.contains(target) || !self.controls.includes(target))) { e2.stopImmediatePropagation(); e2.stopPropagation(); @@ -639,19 +474,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy function carouselPointerDownHandler(e2) { const { target } = e2; const self = getCarouselInstance(this); - // istanbul ignore else @preserve - if (self && !self.isAnimating && !self.isTouch) { - const { controls, indicators } = self; - // istanbul ignore else @preserve - if (![...controls, ...indicators].every( - (el) => el === target || el.contains(target) - )) { - startX = e2.pageX; - // istanbul ignore else @preserve - if (this.contains(target)) { - self.isTouch = true; - toggleCarouselTouchHandlers(self, true); - } + if (!self || self.isAnimating || self.isTouch) return; + const { controls, indicators } = self; + if (![...controls, ...indicators].every( + (el) => el === target || el.contains(target) + )) { + startX = e2.pageX; + if (this.contains(target)) { + self.isTouch = true; + toggleCarouselTouchHandlers(self, true); } } } @@ -659,189 +490,130 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy currentX = e2.pageX; }; const carouselPointerUpHandler = (e2) => { - var _a; const { target } = e2; const doc = d(target); const self = [...ue(carouselSelector, doc)].map((c) => getCarouselInstance(c)).find((i2) => i2.isTouch); - // istanbul ignore else @preserve - if (self) { - const { element, index } = self; - const RTL = To(element); - endX = e2.pageX; - self.isTouch = false; - toggleCarouselTouchHandlers(self); - if (!((_a = doc.getSelection()) == null ? void 0 : _a.toString().length) && element.contains(target) && Math.abs(startX - endX) > 120) { - // istanbul ignore else @preserve - if (currentX < startX) { - self.to(index + (RTL ? -1 : 1)); - } else if (currentX > startX) { - self.to(index + (RTL ? 1 : -1)); - } - } - startX = 0; - currentX = 0; - endX = 0; - } + if (!self) return; + const { element, index } = self; + const RTL = Bo(element); + endX = e2.pageX; + self.isTouch = false; + toggleCarouselTouchHandlers(self); + if (!doc.getSelection()?.toString().length && element.contains(target) && Math.abs(startX - endX) > 120) { + if (currentX < startX) { + self.to(index + (RTL ? -1 : 1)); + } else if (currentX > startX) { + self.to(index + (RTL ? 1 : -1)); + } + } + startX = 0; + currentX = 0; + endX = 0; }; const activateCarouselIndicator = (self, index) => { const { indicators } = self; - [...indicators].forEach((x2) => Fn(x2, activeClass)); - // istanbul ignore else @preserve - if (self.indicators[index]) Bn(indicators[index], activeClass); + [...indicators].forEach((x2) => qn(x2, activeClass)); + if (self.indicators[index]) Kn(indicators[index], activeClass); }; const toggleCarouselTouchHandlers = (self, add) => { const { element } = self; - const action = add ? E$1 : r; + const action = add ? E : r; action( d(element), Ot, carouselPointerMoveHandler, - eo + go ); action( d(element), - Lt, + xt, carouselPointerUpHandler, - eo + go ); }; const getActiveIndex = (self) => { const { slides, element } = self; - const activeItem = Co(`.${carouselItem}.${activeClass}`, element); - return l(activeItem) ? [...slides].indexOf(activeItem) : -1; + const activeItem = Ho( + `.${carouselItem}.${activeClass}`, + element + ); + return activeItem ? [...slides].indexOf(activeItem) : -1; }; class Carousel extends BaseComponent { - /** - * @param target mostly a `.carousel` element - * @param config instance options - */ + static selector = carouselSelector; + static init = carouselInitCallback; + static getInstance = getCarouselInstance; constructor(target, config) { super(target, config); - /** - * Toggles all event listeners for the `Carousel` instance. - * - * @param add when `TRUE` event listeners are added - */ - __publicField(this, "_toggleEventListeners", (add) => { - const { element, options, slides, controls, indicators } = this; - const { touch, pause, interval, keyboard } = options; - const action = add ? E$1 : r; - if (pause && interval) { - action(element, bt, carouselPauseHandler); - action(element, ht, carouselResumeHandler); - } - if (touch && slides.length > 2) { - action( - element, - Dt, - carouselPointerDownHandler, - eo - ); - action(element, Ut, carouselDragHandler, { passive: false }); - action(element, Ve, carouselDragHandler, { passive: false }); - } - // istanbul ignore else @preserve - if (controls.length) { - controls.forEach((arrow) => { - // istanbul ignore else @preserve - if (arrow) action(arrow, ft, carouselControlsHandler); - }); - } - // istanbul ignore else @preserve - if (indicators.length) { - indicators.forEach((indicator) => { - action(indicator, ft, carouselIndicatorHandler); - }); - } - if (keyboard) { - action(d(element), ut, carouselKeyHandler); - } - }); const { element } = this; - this.direction = To(element) ? "right" : "left"; + this.direction = Bo(element) ? "right" : "left"; this.isTouch = false; - this.slides = Io(carouselItem, element); + this.slides = Ro(carouselItem, element); const { slides } = this; - if (slides.length >= 2) { - const activeIndex = getActiveIndex(this); - const transitionItem = [...slides].find( - (s) => xo(s, `.${carouselItem}-next,.${carouselItem}-next`) - ); - this.index = activeIndex; - const doc = d(element); - this.controls = [ - ...ue(`[${dataBsSlide}]`, element), - ...ue( - `[${dataBsSlide}][${dataBsTarget}="#${element.id}"]`, - doc - ) - ].filter((c, i2, ar) => i2 === ar.indexOf(c)); - this.indicator = Co(`.${carouselString}-indicators`, element); - this.indicators = [ - ...this.indicator ? ue(`[${dataBsSlideTo}]`, this.indicator) : [], - ...ue( - `[${dataBsSlideTo}][${dataBsTarget}="#${element.id}"]`, - doc - ) - ].filter((c, i2, ar) => i2 === ar.indexOf(c)); - const { options } = this; - this.options.interval = options.interval === true ? carouselDefaults.interval : options.interval; - // istanbul ignore next @preserve - impossible to test - if (transitionItem) { - this.index = [...slides].indexOf(transitionItem); - } else if (activeIndex < 0) { - this.index = 0; - Bn(slides[0], activeClass); - if (this.indicators.length) activateCarouselIndicator(this, 0); - } - // istanbul ignore else @preserve - if (this.indicators.length) activateCarouselIndicator(this, this.index); - this._toggleEventListeners(true); - if (options.interval) this.cycle(); - } + if (slides.length < 2) return; + const activeIndex = getActiveIndex(this); + const transitionItem = [...slides].find( + (s) => ve(s, `.${carouselItem}-next`) + ); + this.index = activeIndex; + const doc = d(element); + this.controls = [ + ...ue(`[${dataBsSlide}]`, element), + ...ue( + `[${dataBsSlide}][${dataBsTarget}="#${element.id}"]`, + doc + ) + ].filter((c, i2, ar) => i2 === ar.indexOf(c)); + this.indicator = Ho( + `.${carouselString}-indicators`, + element + ); + this.indicators = [ + ...this.indicator ? ue(`[${dataBsSlideTo}]`, this.indicator) : [], + ...ue( + `[${dataBsSlideTo}][${dataBsTarget}="#${element.id}"]`, + doc + ) + ].filter((c, i2, ar) => i2 === ar.indexOf(c)); + const { options } = this; + this.options.interval = options.interval === true ? carouselDefaults.interval : options.interval; + if (transitionItem) { + this.index = [...slides].indexOf(transitionItem); + } else if (activeIndex < 0) { + this.index = 0; + Kn(slides[0], activeClass); + if (this.indicators.length) activateCarouselIndicator(this, 0); + } + if (this.indicators.length) activateCarouselIndicator(this, this.index); + this._toggleEventListeners(true); + if (options.interval) this.cycle(); } - /** - * Returns component name string. - */ get name() { return carouselComponent; } - /** - * Returns component default options. - */ get defaults() { return carouselDefaults; } - /** - * Check if instance is paused. - */ get isPaused() { - return Hn(this.element, pausedClass); + return Gn(this.element, pausedClass); } - /** - * Check if instance is animating. - */ get isAnimating() { - return Co( + return Ho( `.${carouselItem}-next,.${carouselItem}-prev`, this.element ) !== null; } - // CAROUSEL PUBLIC METHODS - // ======================= - /** Slide automatically through items. */ cycle() { const { element, options, isPaused, index } = this; - so.clear(element, carouselString); + bo.clear(element, carouselString); if (isPaused) { - so.clear(element, pausedClass); - Fn(element, pausedClass); + bo.clear(element, pausedClass); + qn(element, pausedClass); } - so.set( + bo.set( element, () => { - // istanbul ignore else @preserve - if (this.element && !this.isPaused && !this.isTouch && vo(element)) { + if (this.element && !this.isPaused && !this.isTouch && To(element)) { this.to(index + 1); } }, @@ -849,109 +621,124 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy carouselString ); } - /** Pause the automatic cycle. */ pause() { const { element, options } = this; - // istanbul ignore else @preserve - if (!this.isPaused && options.interval) { - Bn(element, pausedClass); - so.set( - element, - () => { - }, - 1, - pausedClass - ); - } + if (this.isPaused || !options.interval) return; + Kn(element, pausedClass); + bo.set( + element, + () => { + }, + 1, + pausedClass + ); } - /** Slide to the next item. */ next() { - // istanbul ignore else @preserve if (!this.isAnimating) { this.to(this.index + 1); } } - /** Slide to the previous item. */ prev() { - // istanbul ignore else @preserve if (!this.isAnimating) { this.to(this.index - 1); } } - /** - * Jump to the item with the `idx` index. - * - * @param idx the index of the item to jump to - */ to(idx) { const { element, slides, options } = this; const activeItem = getActiveIndex(this); - const RTL = To(element); + const RTL = Bo(element); let next = idx; - if (!this.isAnimating && activeItem !== next && !so.get(element, dataBsSlide)) { - // istanbul ignore else @preserve - if (activeItem < next || activeItem === 0 && next === slides.length - 1) { - this.direction = RTL ? "right" : "left"; - } else if (activeItem > next || activeItem === slides.length - 1 && next === 0) { - this.direction = RTL ? "left" : "right"; - } - const { direction } = this; - if (next < 0) { - next = slides.length - 1; - } else if (next >= slides.length) { - next = 0; - } - const orientation = direction === "left" ? "next" : "prev"; - const directionClass = direction === "left" ? "start" : "end"; - const eventProperties = { - relatedTarget: slides[next], - from: activeItem, - to: next, - direction - }; - T(carouselSlideEvent, eventProperties); - T(carouselSlidEvent, eventProperties); - K(element, carouselSlideEvent); - if (!carouselSlideEvent.defaultPrevented) { - this.index = next; - activateCarouselIndicator(this, next); - if (re(slides[next]) && Hn(element, "slide")) { - so.set( - element, - () => { - Bn(slides[next], `${carouselItem}-${orientation}`); - no(slides[next]); - Bn(slides[next], `${carouselItem}-${directionClass}`); - Bn(slides[activeItem], `${carouselItem}-${directionClass}`); - qn( - slides[next], - () => this.slides && this.slides.length && carouselTransitionEndHandler(this) - ); - }, - 0, - dataBsSlide - ); - } else { - Bn(slides[next], activeClass); - Fn(slides[activeItem], activeClass); - so.set( - element, - () => { - so.clear(element, dataBsSlide); - // istanbul ignore else @preserve - if (element && options.interval && !this.isPaused) { - this.cycle(); - } - K(element, carouselSlidEvent); - }, - 0, - dataBsSlide + if (this.isAnimating || activeItem === next || bo.get(element, dataBsSlide)) return; + if (activeItem < next || activeItem === 0 && next === slides.length - 1) { + this.direction = RTL ? "right" : "left"; + } else if (activeItem > next || activeItem === slides.length - 1 && next === 0) { + this.direction = RTL ? "left" : "right"; + } + const { direction } = this; + if (next < 0) { + next = slides.length - 1; + } else if (next >= slides.length) { + next = 0; + } + const orientation = direction === "left" ? "next" : "prev"; + const directionClass = direction === "left" ? "start" : "end"; + const eventProperties = { + relatedTarget: slides[next], + from: activeItem, + to: next, + direction + }; + C(carouselSlideEvent, eventProperties); + C(carouselSlidEvent, eventProperties); + G(element, carouselSlideEvent); + if (carouselSlideEvent.defaultPrevented) return; + this.index = next; + activateCarouselIndicator(this, next); + if (ae(slides[next]) && Gn(element, "slide")) { + bo.set( + element, + () => { + Kn(slides[next], `${carouselItem}-${orientation}`); + mo(slides[next]); + Kn(slides[next], `${carouselItem}-${directionClass}`); + Kn(slides[activeItem], `${carouselItem}-${directionClass}`); + no( + slides[next], + () => this.slides && this.slides.length && carouselTransitionEndHandler(this) ); - } - } + }, + 0, + dataBsSlide + ); + } else { + Kn(slides[next], activeClass); + qn(slides[activeItem], activeClass); + bo.set( + element, + () => { + bo.clear(element, dataBsSlide); + if (element && options.interval && !this.isPaused) { + this.cycle(); + } + G(element, carouselSlidEvent); + }, + 0, + dataBsSlide + ); } } - /** Remove `Carousel` component from target. */ + _toggleEventListeners = (add) => { + const { element, options, slides, controls, indicators } = this; + const { touch, pause, interval, keyboard } = options; + const action = add ? E : r; + if (pause && interval) { + action(element, ht, carouselPauseHandler); + action(element, yt, carouselResumeHandler); + } + if (touch && slides.length > 2) { + action( + element, + Dt, + carouselPointerDownHandler, + go + ); + action(element, Wt, carouselDragHandler, { passive: false }); + action(element, Re, carouselDragHandler, { passive: false }); + } + if (controls.length) { + controls.forEach((arrow) => { + action(arrow, gt, carouselControlsHandler); + }); + } + if (indicators.length) { + indicators.forEach((indicator) => { + action(indicator, gt, carouselIndicatorHandler); + }); + } + if (keyboard) { + action(d(element), lt, carouselKeyHandler); + } + }; dispose() { const { isAnimating } = this; const clone = { @@ -960,140 +747,108 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; this._toggleEventListeners(); super.dispose(); - // istanbul ignore next @preserve - impossible to test if (clone.isAnimating) { - qn(clone.slides[clone.index], () => { + no(clone.slides[clone.index], () => { carouselTransitionEndHandler(clone); }); } } } - __publicField(Carousel, "selector", carouselSelector); - __publicField(Carousel, "init", carouselInitCallback); - __publicField(Carousel, "getInstance", getCarouselInstance); const collapsingClass = "collapsing"; const collapseString = "collapse"; const collapseComponent = "Collapse"; const collapseSelector = `.${collapseString}`; const collapseToggleSelector = `[${dataBsToggle}="${collapseString}"]`; const collapseDefaults = { parent: null }; - const getCollapseInstance = (element) => Rn(element, collapseComponent); + const getCollapseInstance = (element) => Xn(element, collapseComponent); const collapseInitCallback = (element) => new Collapse(element); - const showCollapseEvent = to(`show.bs.${collapseString}`); - const shownCollapseEvent = to(`shown.bs.${collapseString}`); - const hideCollapseEvent = to(`hide.bs.${collapseString}`); - const hiddenCollapseEvent = to(`hidden.bs.${collapseString}`); + const showCollapseEvent = po(`show.bs.${collapseString}`); + const shownCollapseEvent = po(`shown.bs.${collapseString}`); + const hideCollapseEvent = po(`hide.bs.${collapseString}`); + const hiddenCollapseEvent = po(`hidden.bs.${collapseString}`); const expandCollapse = (self) => { const { element, parent, triggers } = self; - K(element, showCollapseEvent); + G(element, showCollapseEvent); if (!showCollapseEvent.defaultPrevented) { - so.set(element, _t, 17); - if (parent) so.set(parent, _t, 17); - Bn(element, collapsingClass); - Fn(element, collapseString); - oo(element, { height: `${element.scrollHeight}px` }); - qn(element, () => { - so.clear(element); - if (parent) so.clear(parent); - triggers.forEach((btn) => In(btn, Ae, "true")); - Fn(element, collapsingClass); - Bn(element, collapseString); - Bn(element, showClass); - oo(element, { height: "" }); - K(element, shownCollapseEvent); + bo.set(element, Yt, 17); + if (parent) bo.set(parent, Yt, 17); + Kn(element, collapsingClass); + qn(element, collapseString); + vo(element, { height: `${element.scrollHeight}px` }); + no(element, () => { + bo.clear(element); + if (parent) bo.clear(parent); + triggers.forEach((btn) => Wn(btn, De, "true")); + qn(element, collapsingClass); + Kn(element, collapseString); + Kn(element, showClass); + vo(element, { height: "" }); + G(element, shownCollapseEvent); }); } }; const collapseContent = (self) => { const { element, parent, triggers } = self; - K(element, hideCollapseEvent); + G(element, hideCollapseEvent); if (!hideCollapseEvent.defaultPrevented) { - so.set(element, _t, 17); - if (parent) so.set(parent, _t, 17); - oo(element, { height: `${element.scrollHeight}px` }); - Fn(element, collapseString); - Fn(element, showClass); - Bn(element, collapsingClass); - no(element); - oo(element, { height: "0px" }); - qn(element, () => { - so.clear(element); - // istanbul ignore else @preserve - if (parent) so.clear(parent); - triggers.forEach((btn) => In(btn, Ae, "false")); - Fn(element, collapsingClass); - Bn(element, collapseString); - oo(element, { height: "" }); - K(element, hiddenCollapseEvent); + bo.set(element, Yt, 17); + if (parent) bo.set(parent, Yt, 17); + vo(element, { height: `${element.scrollHeight}px` }); + qn(element, collapseString); + qn(element, showClass); + Kn(element, collapsingClass); + mo(element); + vo(element, { height: "0px" }); + no(element, () => { + bo.clear(element); + if (parent) bo.clear(parent); + triggers.forEach((btn) => Wn(btn, De, "false")); + qn(element, collapsingClass); + Kn(element, collapseString); + vo(element, { height: "" }); + G(element, hiddenCollapseEvent); }); } }; const collapseClickHandler = (e2) => { const { target } = e2; - const trigger = target && Ee(target, collapseToggleSelector); + const trigger = target && Se(target, collapseToggleSelector); const element = trigger && getTargetElement(trigger); const self = element && getCollapseInstance(element); - // istanbul ignore else @preserve - if (self) self.toggle(); - if (trigger && trigger.tagName === "A") e2.preventDefault(); + if (trigger && isDisabled(trigger)) return; + if (!self) return; + self.toggle(); + if (trigger?.tagName === "A") e2.preventDefault(); }; class Collapse extends BaseComponent { - /** - * @param target and `Element` that matches the selector - * @param config instance options - */ + static selector = collapseSelector; + static init = collapseInitCallback; + static getInstance = getCollapseInstance; constructor(target, config) { super(target, config); - /** - * Toggles on/off the event listener(s) of the `Collapse` instance. - * - * @param add when `true`, the event listener is added - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - const { triggers } = this; - // istanbul ignore else @preserve - if (triggers.length) { - triggers.forEach( - (btn) => action(btn, ft, collapseClickHandler) - ); - } - }); const { element, options } = this; const doc = d(element); this.triggers = [...ue(collapseToggleSelector, doc)].filter( (btn) => getTargetElement(btn) === element ); - this.parent = l(options.parent) ? options.parent : M(options.parent) ? getTargetElement(element) || Co(options.parent, doc) : null; + this.parent = b(options.parent) ? options.parent : N(options.parent) ? getTargetElement(element) || Ho(options.parent, doc) : null; this._toggleEventListeners(true); } - /** - * Returns component name string. - */ get name() { return collapseComponent; } - /** - * Returns component default options. - */ get defaults() { return collapseDefaults; } - // COLLAPSE PUBLIC METHODS - // ======================= - /** Hides the collapse. */ hide() { const { triggers, element } = this; - // istanbul ignore else @preserve - if (!so.get(element)) { + if (!bo.get(element)) { collapseContent(this); - // istanbul ignore else @preserve if (triggers.length) { - triggers.forEach((btn) => Bn(btn, `${collapseString}d`)); + triggers.forEach((btn) => Kn(btn, `${collapseString}d`)); } } } - /** Shows the collapse. */ show() { const { element, parent, triggers } = this; let activeCollapse; @@ -1104,46 +859,146 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ].find((i2) => getCollapseInstance(i2)); activeCollapseInstance = activeCollapse && getCollapseInstance(activeCollapse); } - if ((!parent || !so.get(parent)) && !so.get(element)) { + if ((!parent || !bo.get(parent)) && !bo.get(element)) { if (activeCollapseInstance && activeCollapse !== element) { collapseContent(activeCollapseInstance); activeCollapseInstance.triggers.forEach((btn) => { - Bn(btn, `${collapseString}d`); + Kn(btn, `${collapseString}d`); }); } expandCollapse(this); - // istanbul ignore else @preserve if (triggers.length) { - triggers.forEach((btn) => Fn(btn, `${collapseString}d`)); + triggers.forEach((btn) => qn(btn, `${collapseString}d`)); } } } - /** Toggles the visibility of the collapse. */ toggle() { - if (!Hn(this.element, showClass)) this.show(); + if (!Gn(this.element, showClass)) this.show(); else this.hide(); } - /** Remove the `Collapse` component from the target `Element`. */ + _toggleEventListeners = (add) => { + const action = add ? E : r; + const { triggers } = this; + if (triggers.length) { + triggers.forEach((btn) => { + action(btn, gt, collapseClickHandler); + }); + } + }; dispose() { this._toggleEventListeners(); super.dispose(); } } - __publicField(Collapse, "selector", collapseSelector); - __publicField(Collapse, "init", collapseInitCallback); - __publicField(Collapse, "getInstance", getCollapseInstance); + const m = (e2) => e2 != null && typeof e2 == "object" || false, p = (e2) => m(e2) && typeof e2.nodeType == "number" && [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].some( + (t) => e2.nodeType === t + ) || false, h = (e2) => p(e2) && e2.nodeType === 1 || false, w = (e2) => typeof e2 == "function" || false, k = "1.0.2", a = "PositionObserver Error"; + class v { + entries; + static version = k; + _tick; + _root; + _callback; + /** + * The constructor takes two arguments, a `callback`, which is called + * whenever the position of an observed element changes and an `options` object. + * The callback function should take an array of `PositionObserverEntry` objects + * as its only argument, but it's not required. + * + * @param callback the callback that applies to all targets of this observer + * @param options the options of this observer + */ + constructor(t, i2) { + if (!w(t)) + throw new Error(`${a}: ${t} is not a function.`); + this.entries = /* @__PURE__ */ new Map(), this._callback = t, this._root = h(i2?.root) ? i2.root : document?.documentElement, this._tick = 0; + } + /** + * Start observing the position of the specified element. + * If the element is not currently attached to the DOM, + * it will NOT be added to the entries. + * + * @param target an `Element` target + */ + observe = (t) => { + if (!h(t)) + throw new Error( + `${a}: ${t} is not an instance of Element.` + ); + this._root.contains(t) && this._new(t).then((i2) => { + i2 && !this.getEntry(t) && this.entries.set(t, i2), this._tick || (this._tick = requestAnimationFrame(this._runCallback)); + }); + }; + /** + * Stop observing the position of the specified element. + * + * @param target an `HTMLElement` target + */ + unobserve = (t) => { + this.entries.has(t) && this.entries.delete(t); + }; + /** + * Private method responsible for all the heavy duty, + * the observer's runtime. + */ + _runCallback = () => { + if (!this.entries.size) return; + const t = new Promise((i2) => { + const r2 = []; + this.entries.forEach( + ({ target: s, boundingClientRect: n }) => { + this._root.contains(s) && this._new(s).then(({ boundingClientRect: o, isIntersecting: u2 }) => { + if (!u2) return; + const { left: f2, top: _, bottom: l, right: b2 } = o; + if (n.top !== _ || n.left !== f2 || n.right !== b2 || n.bottom !== l) { + const c = { target: s, boundingClientRect: o }; + this.entries.set(s, c), r2.push(c); + } + }); + } + ), i2(r2); + }); + this._tick = requestAnimationFrame(async () => { + const i2 = await t; + i2.length && this._callback(i2, this), this._runCallback(); + }); + }; + /** + * Calculate the target bounding box and determine + * the value of `isVisible`. + * + * @param target an `Element` target + */ + _new = (t) => new Promise((i2) => { + new IntersectionObserver( + ([s], n) => { + n.disconnect(), i2(s); + } + ).observe(t); + }); + /** + * Find the entry for a given target. + * + * @param target an `HTMLElement` target + */ + getEntry = (t) => this.entries.get(t); + /** + * Immediately stop observing all elements. + */ + disconnect = () => { + cancelAnimationFrame(this._tick), this.entries.clear(), this._tick = 0; + }; + } const dropdownMenuClasses = ["dropdown", "dropup", "dropstart", "dropend"]; const dropdownComponent = "Dropdown"; const dropdownMenuClass = "dropdown-menu"; const isEmptyAnchor = (element) => { - const parentAnchor = Ee(element, "A"); - return element.tagName === "A" && // anchor href starts with # - te(element, "href") && j(element, "href").slice(-1) === "#" || // OR a child of an anchor with href starts with # - parentAnchor && te(parentAnchor, "href") && j(parentAnchor, "href").slice(-1) === "#"; + const parentAnchor = Se(element, "A"); + return element.tagName === "A" && ee(element, "href") && j(element, "href")?.slice(-1) === "#" || parentAnchor && ee(parentAnchor, "href") && j(parentAnchor, "href")?.slice(-1) === "#"; }; const [dropdownString, dropupString, dropstartString, dropendString] = dropdownMenuClasses; const dropdownSelector = `[${dataBsToggle}="${dropdownString}"]`; - const getDropdownInstance = (element) => Rn(element, dropdownComponent); + const getDropdownInstance = (element) => Xn(element, dropdownComponent); const dropdownInitCallback = (element) => new Dropdown(element); const dropdownMenuEndClass = `${dropdownMenuClass}-end`; const verticalClass = [dropdownString, dropupString]; @@ -1151,120 +1006,109 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const menuFocusTags = ["A", "BUTTON"]; const dropdownDefaults = { offset: 5, - // [number] 5(px) display: "dynamic" - // [dynamic|static] }; - const showDropdownEvent = to( + const showDropdownEvent = po( `show.bs.${dropdownString}` ); - const shownDropdownEvent = to( + const shownDropdownEvent = po( `shown.bs.${dropdownString}` ); - const hideDropdownEvent = to( + const hideDropdownEvent = po( `hide.bs.${dropdownString}` ); - const hiddenDropdownEvent = to(`hidden.bs.${dropdownString}`); - const updatedDropdownEvent = to(`updated.bs.${dropdownString}`); + const hiddenDropdownEvent = po(`hidden.bs.${dropdownString}`); + const updatedDropdownEvent = po(`updated.bs.${dropdownString}`); const styleDropdown = (self) => { const { element, menu, parentElement, options } = self; const { offset } = options; - // istanbul ignore else @preserve: this test requires a navbar - if (g(menu, "position") !== "static") { - const RTL = To(element); - const menuEnd = Hn(menu, dropdownMenuEndClass); - const resetProps = ["margin", "top", "bottom", "left", "right"]; - resetProps.forEach((p) => { - const style = {}; - style[p] = ""; - oo(menu, style); + if (f$1(menu, "position") === "static") return; + const RTL = Bo(element); + const menuEnd = Gn(menu, dropdownMenuEndClass); + const resetProps = ["margin", "top", "bottom", "left", "right"]; + resetProps.forEach((p2) => { + const style = {}; + style[p2] = ""; + vo(menu, style); + }); + let positionClass = dropdownMenuClasses.find((c) => Gn(parentElement, c)) || dropdownString; + const dropdownMargin = { + dropdown: [offset, 0, 0], + dropup: [0, 0, offset], + dropstart: RTL ? [-1, 0, 0, offset] : [-1, offset, 0], + dropend: RTL ? [-1, offset, 0] : [-1, 0, 0, offset] + }; + const dropdownPosition = { + dropdown: { top: "100%" }, + dropup: { top: "auto", bottom: "100%" }, + dropstart: RTL ? { left: "100%", right: "auto" } : { left: "auto", right: "100%" }, + dropend: RTL ? { left: "auto", right: "100%" } : { left: "100%", right: "auto" }, + menuStart: RTL ? { right: "0", left: "auto" } : { right: "auto", left: "0" }, + menuEnd: RTL ? { right: "auto", left: "0" } : { right: "0", left: "auto" } + }; + const { offsetWidth: menuWidth, offsetHeight: menuHeight } = menu; + const { clientWidth, clientHeight } = w$1(element); + const { + left: targetLeft, + top: targetTop, + width: targetWidth, + height: targetHeight + } = y(element); + const leftFullExceed = targetLeft - menuWidth - offset < 0; + const rightFullExceed = targetLeft + menuWidth + targetWidth + offset >= clientWidth; + const bottomExceed = targetTop + menuHeight + offset >= clientHeight; + const bottomFullExceed = targetTop + menuHeight + targetHeight + offset >= clientHeight; + const topExceed = targetTop - menuHeight - offset < 0; + const leftExceed = (!RTL && menuEnd || RTL && !menuEnd) && targetLeft + targetWidth - menuWidth < 0; + const rightExceed = (RTL && menuEnd || !RTL && !menuEnd) && targetLeft + menuWidth >= clientWidth; + if (horizontalClass.includes(positionClass) && leftFullExceed && rightFullExceed) { + positionClass = dropdownString; + } + if (positionClass === dropstartString && (!RTL ? leftFullExceed : rightFullExceed)) { + positionClass = dropendString; + } + if (positionClass === dropendString && (RTL ? leftFullExceed : rightFullExceed)) { + positionClass = dropstartString; + } + if (positionClass === dropupString && topExceed && !bottomFullExceed) { + positionClass = dropdownString; + } + if (positionClass === dropdownString && bottomFullExceed && !topExceed) { + positionClass = dropupString; + } + if (horizontalClass.includes(positionClass) && bottomExceed) { + C(dropdownPosition[positionClass], { + top: "auto", + bottom: 0 }); - let positionClass = dropdownMenuClasses.find( - (c) => Hn(parentElement, c) - ) || // istanbul ignore next @preserve: fallback position - dropdownString; - const dropdownMargin = { - dropdown: [offset, 0, 0], - dropup: [0, 0, offset], - dropstart: RTL ? [-1, 0, 0, offset] : [-1, offset, 0], - dropend: RTL ? [-1, offset, 0] : [-1, 0, 0, offset] - }; - const dropdownPosition = { - dropdown: { top: "100%" }, - dropup: { top: "auto", bottom: "100%" }, - dropstart: RTL ? { left: "100%", right: "auto" } : { left: "auto", right: "100%" }, - dropend: RTL ? { left: "auto", right: "100%" } : { left: "100%", right: "auto" }, - menuStart: RTL ? { right: "0", left: "auto" } : { right: "auto", left: "0" }, - menuEnd: RTL ? { right: "auto", left: "0" } : { right: "0", left: "auto" } - }; - const { offsetWidth: menuWidth, offsetHeight: menuHeight } = menu; - const { clientWidth, clientHeight } = k(element); - const { - left: targetLeft, - top: targetTop, - width: targetWidth, - height: targetHeight - } = h(element); - const leftFullExceed = targetLeft - menuWidth - offset < 0; - const rightFullExceed = targetLeft + menuWidth + targetWidth + offset >= clientWidth; - const bottomExceed = targetTop + menuHeight + offset >= clientHeight; - const bottomFullExceed = targetTop + menuHeight + targetHeight + offset >= clientHeight; - const topExceed = targetTop - menuHeight - offset < 0; - const leftExceed = (!RTL && menuEnd || RTL && !menuEnd) && targetLeft + targetWidth - menuWidth < 0; - const rightExceed = (RTL && menuEnd || !RTL && !menuEnd) && targetLeft + menuWidth >= clientWidth; - if (horizontalClass.includes(positionClass) && leftFullExceed && rightFullExceed) { - positionClass = dropdownString; - } - if (positionClass === dropstartString && (!RTL ? leftFullExceed : rightFullExceed)) { - positionClass = dropendString; - } - if (positionClass === dropendString && (RTL ? leftFullExceed : rightFullExceed)) { - positionClass = dropstartString; - } - if (positionClass === dropupString && topExceed && !bottomFullExceed) { - positionClass = dropdownString; - } - if (positionClass === dropdownString && bottomFullExceed && !topExceed) { - positionClass = dropupString; - } - if (horizontalClass.includes(positionClass) && bottomExceed) { - T(dropdownPosition[positionClass], { - top: "auto", - bottom: 0 - }); + } + if (verticalClass.includes(positionClass) && (leftExceed || rightExceed)) { + let posAjust = { left: "auto", right: "auto" }; + if (!leftExceed && rightExceed && !RTL) { + posAjust = { left: "auto", right: 0 }; } - if (verticalClass.includes(positionClass) && (leftExceed || rightExceed)) { - let posAjust = { left: "auto", right: "auto" }; - // istanbul ignore else @preserve - if (!leftExceed && rightExceed && !RTL) { - posAjust = { left: "auto", right: 0 }; - } - // istanbul ignore else @preserve - if (leftExceed && !rightExceed && RTL) { - posAjust = { left: 0, right: "auto" }; - } - // istanbul ignore else @preserve - if (posAjust) { - T(dropdownPosition[positionClass], posAjust); - } + if (leftExceed && !rightExceed && RTL) { + posAjust = { left: 0, right: "auto" }; } - const margins = dropdownMargin[positionClass]; - oo(menu, { - ...dropdownPosition[positionClass], - margin: `${margins.map((x2) => x2 ? `${x2}px` : x2).join(" ")}` - }); - if (verticalClass.includes(positionClass) && menuEnd) { - // istanbul ignore else @preserve - if (menuEnd) { - const endAdjust = !RTL && leftExceed || RTL && rightExceed ? "menuStart" : "menuEnd"; - oo(menu, dropdownPosition[endAdjust]); - } + if (posAjust) { + C(dropdownPosition[positionClass], posAjust); + } + } + const margins = dropdownMargin[positionClass]; + vo(menu, { + ...dropdownPosition[positionClass], + margin: `${margins.map((x2) => x2 ? `${x2}px` : x2).join(" ")}` + }); + if (verticalClass.includes(positionClass) && menuEnd) { + if (menuEnd) { + const endAdjust = !RTL && leftExceed || RTL && rightExceed ? "menuStart" : "menuEnd"; + vo(menu, dropdownPosition[endAdjust]); } - K(parentElement, updatedDropdownEvent); } + G(parentElement, updatedDropdownEvent); }; const getMenuItems = (menu) => { - return [...menu.children].map((c) => { + return Array.from(menu.children).map((c) => { if (c && menuFocusTags.includes(c.tagName)) return c; const { firstElementChild } = c; if (firstElementChild && menuFocusTags.includes(firstElementChild.tagName)) { @@ -1274,23 +1118,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }).filter((c) => c); }; const toggleDropdownDismiss = (self) => { - const { element, options } = self; - const action = self.open ? E$1 : r; + const { element, options, menu } = self; + const action = self.open ? E : r; const doc = d(element); - action(doc, ft, dropdownDismissHandler); - action(doc, ot, dropdownDismissHandler); - action(doc, ut, dropdownPreventScroll); - action(doc, dt, dropdownKeyHandler); - // istanbul ignore else @preserve + action(doc, gt, dropdownDismissHandler); + action(doc, st, dropdownDismissHandler); + action(doc, lt, dropdownPreventScroll); + action(doc, ft, dropdownKeyHandler); if (options.display === "dynamic") { - [Ht, zt].forEach((ev) => { - action(fo(element), ev, dropdownLayoutHandler, eo); - }); + if (self.open) self._observer.observe(menu); + else self._observer.disconnect(); } }; const getCurrentOpenDropdown = (element) => { const currentParent = [...dropdownMenuClasses, "btn-group", "input-group"].map( - (c) => Io(`${c} ${showClass}`, d(element)) + (c) => Ro(`${c} ${showClass}`, d(element)) ).find((x2) => x2.length); if (currentParent && currentParent.length) { return [...currentParent[0].children].find( @@ -1301,175 +1143,131 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; const dropdownDismissHandler = (e2) => { const { target, type } = e2; - // istanbul ignore else @preserve - if (target && l(target)) { - const element = getCurrentOpenDropdown(target); - const self = element && getDropdownInstance(element); - // istanbul ignore else @preserve - if (self) { - const { parentElement, menu } = self; - const isForm = parentElement && parentElement.contains(target) && (target.tagName === "form" || Ee(target, "form") !== null); - if ([ft, gt].includes(type) && isEmptyAnchor(target)) { - e2.preventDefault(); - } - // istanbul ignore else @preserve - if (!isForm && type !== ot && target !== element && target !== menu) { - self.hide(); - } - } - } - }; - const dropdownClickHandler = (e2) => { - const { target } = e2; - const element = target && Ee(target, dropdownSelector); + if (!b(target)) return; + const element = getCurrentOpenDropdown(target); const self = element && getDropdownInstance(element); - // istanbul ignore else @preserve - if (self) { - e2.stopPropagation(); - self.toggle(); - // istanbul ignore else @preserve - if (element && isEmptyAnchor(element)) e2.preventDefault(); + if (!self) return; + const { parentElement, menu } = self; + const isForm = parentElement && parentElement.contains(target) && (target.tagName === "form" || Se(target, "form") !== null); + if ([gt, vt].includes(type) && isEmptyAnchor(target)) { + e2.preventDefault(); + } + if (!isForm && type !== st && target !== element && target !== menu) { + self.hide(); } }; + function dropdownClickHandler(e2) { + const self = getDropdownInstance(this); + if (isDisabled(this)) return; + if (!self) return; + e2.stopPropagation(); + self.toggle(); + if (isEmptyAnchor(this)) e2.preventDefault(); + } const dropdownPreventScroll = (e2) => { - // istanbul ignore else @preserve - if ([Je, Xe].includes(e2.code)) e2.preventDefault(); + if ([en, nn].includes(e2.code)) e2.preventDefault(); }; function dropdownKeyHandler(e2) { const { code } = e2; const element = getCurrentOpenDropdown(this); - const self = element && getDropdownInstance(element); - const { activeElement } = element && d(element); - // istanbul ignore else @preserve - if (self && activeElement) { - const { menu, open } = self; - const menuItems = getMenuItems(menu); - if (menuItems && menuItems.length && [Je, Xe].includes(code)) { - let idx = menuItems.indexOf(activeElement); - // istanbul ignore else @preserve - if (activeElement === element) { - idx = 0; - } else if (code === Xe) { - idx = idx > 1 ? idx - 1 : 0; - } else if (code === Je) { - idx = idx < menuItems.length - 1 ? idx + 1 : idx; - } - // istanbul ignore else @preserve - if (menuItems[idx]) Jn(menuItems[idx]); - } - if (sn === code && open) { - self.toggle(); - Jn(element); - } + if (!element) return; + const self = getDropdownInstance(element); + const { activeElement } = d(element); + if (!self || !activeElement) return; + const { menu, open } = self; + const menuItems = getMenuItems(menu); + if (menuItems && menuItems.length && [en, nn].includes(code)) { + let idx = menuItems.indexOf(activeElement); + if (activeElement === element) { + idx = 0; + } else if (code === nn) { + idx = idx > 1 ? idx - 1 : 0; + } else if (code === en) { + idx = idx < menuItems.length - 1 ? idx + 1 : idx; + } + if (menuItems[idx]) ro(menuItems[idx]); + } + if (fn === code && open) { + self.toggle(); + ro(element); } } - function dropdownLayoutHandler() { - const element = getCurrentOpenDropdown(this); - const self = element && getDropdownInstance(element); - // istanbul ignore else @preserve - if (self && self.open) styleDropdown(self); - } class Dropdown extends BaseComponent { - /** - * @param target Element or string selector - * @param config the instance options - */ + static selector = dropdownSelector; + static init = dropdownInitCallback; + static getInstance = getDropdownInstance; constructor(target, config) { super(target, config); - /** - * Toggles on/off the `click` event listener of the `Dropdown`. - * - * @param add when `true`, it will add the event listener - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - action(this.element, ft, dropdownClickHandler); - }); const { parentElement } = this.element; - const [menu] = Io( + const [menu] = Ro( dropdownMenuClass, parentElement ); - if (menu) { - this.parentElement = parentElement; - this.menu = menu; - this._toggleEventListeners(true); - } + if (!menu) return; + this.parentElement = parentElement; + this.menu = menu; + this._observer = new v( + () => styleDropdown(this) + ); + this._toggleEventListeners(true); } - /** - * Returns component name string. - */ get name() { return dropdownComponent; } - /** - * Returns component default options. - */ get defaults() { return dropdownDefaults; } - // DROPDOWN PUBLIC METHODS - // ======================= - /** Shows/hides the dropdown menu to the user. */ toggle() { if (this.open) this.hide(); else this.show(); } - /** Shows the dropdown menu to the user. */ show() { const { element, open, menu, parentElement } = this; - // istanbul ignore else @preserve - if (!open) { - const currentElement = getCurrentOpenDropdown(element); - const currentInstance = currentElement && getDropdownInstance(currentElement); - if (currentInstance) currentInstance.hide(); - [showDropdownEvent, shownDropdownEvent, updatedDropdownEvent].forEach( - (e2) => { - e2.relatedTarget = element; - } - ); - K(parentElement, showDropdownEvent); - if (!showDropdownEvent.defaultPrevented) { - Bn(menu, showClass); - Bn(parentElement, showClass); - In(element, Ae, "true"); - styleDropdown(this); - this.open = !open; - Jn(element); - toggleDropdownDismiss(this); - K(parentElement, shownDropdownEvent); + if (open) return; + const currentElement = getCurrentOpenDropdown(element); + const currentInstance = currentElement && getDropdownInstance(currentElement); + if (currentInstance) currentInstance.hide(); + [showDropdownEvent, shownDropdownEvent, updatedDropdownEvent].forEach( + (e2) => { + e2.relatedTarget = element; } - } + ); + G(parentElement, showDropdownEvent); + if (showDropdownEvent.defaultPrevented) return; + Kn(menu, showClass); + Kn(parentElement, showClass); + Wn(element, De, "true"); + styleDropdown(this); + this.open = !open; + ro(element); + toggleDropdownDismiss(this); + G(parentElement, shownDropdownEvent); } - /** Hides the dropdown menu from the user. */ hide() { const { element, open, menu, parentElement } = this; - // istanbul ignore else @preserve - if (open) { - [hideDropdownEvent, hiddenDropdownEvent].forEach((e2) => { - e2.relatedTarget = element; - }); - K(parentElement, hideDropdownEvent); - if (!hideDropdownEvent.defaultPrevented) { - Fn(menu, showClass); - Fn(parentElement, showClass); - In(element, Ae, "false"); - this.open = !open; - toggleDropdownDismiss(this); - K(parentElement, hiddenDropdownEvent); - } - } - } - /** Removes the `Dropdown` component from the target element. */ + if (!open) return; + [hideDropdownEvent, hiddenDropdownEvent].forEach((e2) => { + e2.relatedTarget = element; + }); + G(parentElement, hideDropdownEvent); + if (hideDropdownEvent.defaultPrevented) return; + qn(menu, showClass); + qn(parentElement, showClass); + Wn(element, De, "false"); + this.open = !open; + toggleDropdownDismiss(this); + G(parentElement, hiddenDropdownEvent); + } + _toggleEventListeners = (add) => { + const action = add ? E : r; + action(this.element, gt, dropdownClickHandler); + }; dispose() { if (this.open) this.hide(); this._toggleEventListeners(); super.dispose(); } } - __publicField(Dropdown, "selector", dropdownSelector); - __publicField(Dropdown, "init", dropdownInitCallback); - __publicField(Dropdown, "getInstance", getDropdownInstance); const modalString = "modal"; const modalComponent = "Modal"; const offcanvasComponent = "Offcanvas"; @@ -1478,23 +1276,22 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const stickyTopClass = "sticky-top"; const positionStickyClass = "position-sticky"; const getFixedItems = (parent) => [ - ...Io(fixedTopClass, parent), - ...Io(fixedBottomClass, parent), - ...Io(stickyTopClass, parent), - ...Io(positionStickyClass, parent), - ...Io("is-fixed", parent) + ...Ro(fixedTopClass, parent), + ...Ro(fixedBottomClass, parent), + ...Ro(stickyTopClass, parent), + ...Ro(positionStickyClass, parent), + ...Ro("is-fixed", parent) ]; const resetScrollbar = (element) => { - const bd = ao(element); - oo(bd, { + const bd = wo(element); + vo(bd, { paddingRight: "", overflow: "" }); const fixedItems = getFixedItems(bd); - // istanbul ignore else @preserve if (fixedItems.length) { fixedItems.forEach((fixed) => { - oo(fixed, { + vo(fixed, { paddingRight: "", marginRight: "" }); @@ -1502,47 +1299,41 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } }; const measureScrollbar = (element) => { - const { clientWidth } = k(element); - const { innerWidth } = fo(element); + const { clientWidth } = w$1(element); + const { innerWidth } = ge(element); return Math.abs(innerWidth - clientWidth); }; const setScrollbar = (element, overflow) => { - const bd = ao(element); - const bodyPad = parseInt(g(bd, "paddingRight"), 10); - const isOpen = g(bd, "overflow") === "hidden"; + const bd = wo(element); + const bodyPad = parseInt(f$1(bd, "paddingRight"), 10); + const isOpen = f$1(bd, "overflow") === "hidden"; const sbWidth = isOpen && bodyPad ? 0 : measureScrollbar(element); const fixedItems = getFixedItems(bd); - // istanbul ignore else @preserve - if (overflow) { - oo(bd, { - overflow: "hidden", - paddingRight: `${bodyPad + sbWidth}px` - }); - // istanbul ignore else @preserve - if (fixedItems.length) { - fixedItems.forEach((fixed) => { - const itemPadValue = g(fixed, "paddingRight"); - fixed.style.paddingRight = `${parseInt(itemPadValue, 10) + sbWidth}px`; - // istanbul ignore else @preserve - if ([stickyTopClass, positionStickyClass].some((c) => Hn(fixed, c))) { - const itemMValue = g(fixed, "marginRight"); - fixed.style.marginRight = `${parseInt(itemMValue, 10) - sbWidth}px`; - } - }); + if (!overflow) return; + vo(bd, { + overflow: "hidden", + paddingRight: `${bodyPad + sbWidth}px` + }); + if (!fixedItems.length) return; + fixedItems.forEach((fixed) => { + const itemPadValue = f$1(fixed, "paddingRight"); + fixed.style.paddingRight = `${parseInt(itemPadValue, 10) + sbWidth}px`; + if ([stickyTopClass, positionStickyClass].some((c) => Gn(fixed, c))) { + const itemMValue = f$1(fixed, "marginRight"); + fixed.style.marginRight = `${parseInt(itemMValue, 10) - sbWidth}px`; } - } + }); }; const offcanvasString = "offcanvas"; - const popupContainer = ee({ + const popupContainer = ne({ tagName: "div", className: "popup-container" }); const appendPopup = (target, customContainer) => { - const containerIsBody = i(customContainer) && customContainer.nodeName === "BODY"; - const lookup = i(customContainer) && !containerIsBody ? customContainer : popupContainer; - const BODY = containerIsBody ? customContainer : ao(target); - // istanbul ignore else @preserve - if (i(target)) { + const containerIsBody = u(customContainer) && customContainer.nodeName === "BODY"; + const lookup = u(customContainer) && !containerIsBody ? customContainer : popupContainer; + const BODY = containerIsBody ? customContainer : wo(target); + if (u(target)) { if (lookup === popupContainer) { BODY.append(popupContainer); } @@ -1550,10 +1341,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } }; const removePopup = (target, customContainer) => { - const containerIsBody = i(customContainer) && customContainer.nodeName === "BODY"; - const lookup = i(customContainer) && !containerIsBody ? customContainer : popupContainer; - // istanbul ignore else @preserve - if (i(target)) { + const containerIsBody = u(customContainer) && customContainer.nodeName === "BODY"; + const lookup = u(customContainer) && !containerIsBody ? customContainer : popupContainer; + if (u(target)) { target.remove(); if (lookup === popupContainer && !popupContainer.children.length) { popupContainer.remove(); @@ -1561,17 +1351,17 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } }; const hasPopup = (target, customContainer) => { - const lookup = i(customContainer) && customContainer.nodeName !== "BODY" ? customContainer : popupContainer; - return i(target) && lookup.contains(target); + const lookup = u(customContainer) && customContainer.nodeName !== "BODY" ? customContainer : popupContainer; + return u(target) && lookup.contains(target); }; const backdropString = "backdrop"; const modalBackdropClass = `${modalString}-${backdropString}`; const offcanvasBackdropClass = `${offcanvasString}-${backdropString}`; const modalActiveSelector = `.${modalString}.${showClass}`; const offcanvasActiveSelector = `.${offcanvasString}.${showClass}`; - const overlay = ee("div"); + const overlay = ne("div"); const getCurrentOpen = (element) => { - return Co( + return Ho( `${modalActiveSelector},${offcanvasActiveSelector}`, d(element) ); @@ -1579,33 +1369,33 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const toggleOverlayType = (isModal) => { const targetClass = isModal ? modalBackdropClass : offcanvasBackdropClass; [modalBackdropClass, offcanvasBackdropClass].forEach((c) => { - Fn(overlay, c); + qn(overlay, c); }); - Bn(overlay, targetClass); + Kn(overlay, targetClass); }; const appendOverlay = (element, hasFade, isModal) => { toggleOverlayType(isModal); - appendPopup(overlay, ao(element)); - if (hasFade) Bn(overlay, fadeClass); + appendPopup(overlay, wo(element)); + if (hasFade) Kn(overlay, fadeClass); }; const showOverlay = () => { - if (!Hn(overlay, showClass)) { - Bn(overlay, showClass); - no(overlay); + if (!Gn(overlay, showClass)) { + Kn(overlay, showClass); + mo(overlay); } }; const hideOverlay = () => { - Fn(overlay, showClass); + qn(overlay, showClass); }; const removeOverlay = (element) => { if (!getCurrentOpen(element)) { - Fn(overlay, fadeClass); - removePopup(overlay, ao(element)); + qn(overlay, fadeClass); + removePopup(overlay, wo(element)); resetScrollbar(element); } }; const isVisible = (element) => { - return l(element) && g(element, "visibility") !== "hidden" && element.offsetParent !== null; + return b(element) && f$1(element, "visibility") !== "hidden" && element.offsetParent !== null; }; const modalSelector = `.${modalString}`; const modalToggleSelector = `[${dataBsToggle}="${modalString}"]`; @@ -1615,271 +1405,224 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy backdrop: true, keyboard: true }; - const getModalInstance = (element) => Rn(element, modalComponent); + const getModalInstance = (element) => Xn(element, modalComponent); const modalInitCallback = (element) => new Modal(element); - const showModalEvent = to( + const showModalEvent = po( `show.bs.${modalString}` ); - const shownModalEvent = to( + const shownModalEvent = po( `shown.bs.${modalString}` ); - const hideModalEvent = to( + const hideModalEvent = po( `hide.bs.${modalString}` ); - const hiddenModalEvent = to( + const hiddenModalEvent = po( `hidden.bs.${modalString}` ); const setModalScrollbar = (self) => { const { element } = self; const scrollbarWidth = measureScrollbar(element); - const { clientHeight, scrollHeight } = k(element); + const { clientHeight, scrollHeight } = w$1(element); const { clientHeight: modalHeight, scrollHeight: modalScrollHeight } = element; const modalOverflow = modalHeight !== modalScrollHeight; - // istanbul ignore next @preserve: impossible to test? if (!modalOverflow && scrollbarWidth) { - const pad = !To(element) ? "paddingRight" : "paddingLeft"; + const pad = !Bo(element) ? "paddingRight" : "paddingLeft"; const padStyle = { [pad]: `${scrollbarWidth}px` }; - oo(element, padStyle); + vo(element, padStyle); } setScrollbar(element, modalOverflow || clientHeight !== scrollHeight); }; const toggleModalDismiss = (self, add) => { - const action = add ? E$1 : r; - const { element, update } = self; - action(element, ft, modalDismissHandler); - action(fo(element), zt, update, eo); - action(d(element), ut, modalKeyHandler); + const action = add ? E : r; + const { element } = self; + action(element, gt, modalDismissHandler); + action(d(element), lt, modalKeyHandler); + if (add) self._observer.observe(element); + else self._observer.disconnect(); }; const afterModalHide = (self) => { const { triggers, element, relatedTarget } = self; removeOverlay(element); - oo(element, { paddingRight: "", display: "" }); + vo(element, { paddingRight: "", display: "" }); toggleModalDismiss(self); const focusElement = showModalEvent.relatedTarget || triggers.find(isVisible); - // istanbul ignore else @preserve - if (focusElement) Jn(focusElement); - hiddenModalEvent.relatedTarget = relatedTarget; - K(element, hiddenModalEvent); - ro(element); + if (focusElement) ro(focusElement); + hiddenModalEvent.relatedTarget = relatedTarget || void 0; + G(element, hiddenModalEvent); + yo(element); }; const afterModalShow = (self) => { const { element, relatedTarget } = self; - Jn(element); - toggleModalDismiss(self, true); - shownModalEvent.relatedTarget = relatedTarget; - K(element, shownModalEvent); ro(element); + toggleModalDismiss(self, true); + shownModalEvent.relatedTarget = relatedTarget || void 0; + G(element, shownModalEvent); + yo(element); }; const beforeModalShow = (self) => { const { element, hasFade } = self; - oo(element, { display: "block" }); + vo(element, { display: "block" }); setModalScrollbar(self); - // istanbul ignore else @preserve if (!getCurrentOpen(element)) { - oo(ao(element), { overflow: "hidden" }); + vo(wo(element), { overflow: "hidden" }); } - Bn(element, showClass); - zn(element, X); - In(element, Te, "true"); - if (hasFade) qn(element, () => afterModalShow(self)); + Kn(element, showClass); + Qn(element, $); + Wn(element, ze, "true"); + if (hasFade) no(element, () => afterModalShow(self)); else afterModalShow(self); }; const beforeModalHide = (self) => { const { element, options, hasFade } = self; - if (options.backdrop && hasFade && Hn(overlay, showClass) && !getCurrentOpen(element)) { + if (options.backdrop && hasFade && Gn(overlay, showClass) && !getCurrentOpen(element)) { hideOverlay(); - qn(overlay, () => afterModalHide(self)); + no(overlay, () => afterModalHide(self)); } else { afterModalHide(self); } }; - const modalClickHandler = (e2) => { - const { target } = e2; - const trigger = target && Ee(target, modalToggleSelector); - const element = trigger && getTargetElement(trigger); + function modalClickHandler(e2) { + const element = getTargetElement(this); const self = element && getModalInstance(element); - // istanbul ignore else @preserve - if (self) { - // istanbul ignore else @preserve - if (trigger && trigger.tagName === "A") e2.preventDefault(); - self.relatedTarget = trigger; - self.toggle(); - } - }; + if (isDisabled(this)) return; + if (!self) return; + if (this.tagName === "A") e2.preventDefault(); + self.relatedTarget = this; + self.toggle(); + } const modalKeyHandler = ({ code, target }) => { - const element = Co(modalActiveSelector, d(target)); + const element = Ho(modalActiveSelector, d(target)); const self = element && getModalInstance(element); - // istanbul ignore else @preserve - if (self) { - const { options } = self; - // istanbul ignore else @preserve - if (options.keyboard && code === sn && // the keyboard option is enabled and the key is 27 - Hn(element, showClass)) { - self.relatedTarget = null; - self.hide(); - } + if (!self) return; + const { options } = self; + if (options.keyboard && code === fn && Gn(element, showClass)) { + self.relatedTarget = null; + self.hide(); } }; const modalDismissHandler = (e2) => { - var _a, _b; const { currentTarget } = e2; const self = currentTarget && getModalInstance(currentTarget); - // istanbul ignore else @preserve - if (self && currentTarget && !so.get(currentTarget)) { - const { options, isStatic, modalDialog } = self; - const { backdrop } = options; - const { target } = e2; - const selectedText = (_b = (_a = d(currentTarget)) == null ? void 0 : _a.getSelection()) == null ? void 0 : _b.toString().length; - const targetInsideDialog = modalDialog.contains(target); - const dismiss = target && Ee(target, modalDismissSelector); - // istanbul ignore else @preserve - if (isStatic && !targetInsideDialog) { - so.set( - currentTarget, - () => { - Bn(currentTarget, modalStaticClass); - qn(modalDialog, () => staticTransitionEnd(self)); - }, - 17 - ); - } else if (dismiss || !selectedText && !isStatic && !targetInsideDialog && backdrop) { - self.relatedTarget = dismiss || null; - self.hide(); - e2.preventDefault(); - } + if (!self || !currentTarget || bo.get(currentTarget)) return; + const { options, isStatic, modalDialog } = self; + const { backdrop } = options; + const { target } = e2; + const selectedText = d(currentTarget)?.getSelection()?.toString().length; + const targetInsideDialog = modalDialog.contains(target); + const dismiss = target && Se(target, modalDismissSelector); + if (isStatic && !targetInsideDialog) { + bo.set( + currentTarget, + () => { + Kn(currentTarget, modalStaticClass); + no(modalDialog, () => staticTransitionEnd(self)); + }, + 17 + ); + } else if (dismiss || !selectedText && !isStatic && !targetInsideDialog && backdrop) { + self.relatedTarget = dismiss || null; + self.hide(); + e2.preventDefault(); } }; const staticTransitionEnd = (self) => { const { element, modalDialog } = self; - const duration = (re(modalDialog) || 0) + 17; - Fn(element, modalStaticClass); - so.set(element, () => so.clear(element), duration); + const duration = (ae(modalDialog) || 0) + 17; + qn(element, modalStaticClass); + bo.set(element, () => bo.clear(element), duration); }; class Modal extends BaseComponent { - /** - * @param target usually the `.modal` element - * @param config instance options - */ + static selector = modalSelector; + static init = modalInitCallback; + static getInstance = getModalInstance; constructor(target, config) { super(target, config); - /** - * Updates the modal layout. - */ - __publicField(this, "update", () => { - // istanbul ignore else @preserve - if (Hn(this.element, showClass)) setModalScrollbar(this); - }); - /** - * Toggles on/off the `click` event listener of the `Modal` instance. - * - * @param add when `true`, event listener(s) is/are added - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - const { triggers } = this; - // istanbul ignore else @preserve - if (triggers.length) { - triggers.forEach( - (btn) => action(btn, ft, modalClickHandler) - ); - } - }); const { element } = this; - const modalDialog = Co(`.${modalString}-dialog`, element); - // istanbul ignore else @preserve - if (modalDialog) { - this.modalDialog = modalDialog; - this.triggers = [ - ...ue(modalToggleSelector, d(element)) - ].filter( - (btn) => getTargetElement(btn) === element - ); - this.isStatic = this.options.backdrop === "static"; - this.hasFade = Hn(element, fadeClass); - this.relatedTarget = null; - this._toggleEventListeners(true); - } + const modalDialog = Ho( + `.${modalString}-dialog`, + element + ); + if (!modalDialog) return; + this.modalDialog = modalDialog; + this.triggers = [ + ...ue( + modalToggleSelector, + d(element) + ) + ].filter( + (btn) => getTargetElement(btn) === element + ); + this.isStatic = this.options.backdrop === "static"; + this.hasFade = Gn(element, fadeClass); + this.relatedTarget = null; + this._observer = new ResizeObserver(() => this.update()); + this._toggleEventListeners(true); } - /** - * Returns component name string. - */ get name() { return modalComponent; } - /** - * Returns component default options. - */ get defaults() { return modalDefaults; } - // MODAL PUBLIC METHODS - // ==================== - /** Toggles the visibility of the modal. */ toggle() { - if (Hn(this.element, showClass)) this.hide(); + if (Gn(this.element, showClass)) this.hide(); else this.show(); } - /** Shows the modal to the user. */ show() { const { element, options, hasFade, relatedTarget } = this; const { backdrop } = options; let overlayDelay = 0; - // istanbul ignore else @preserve - if (!Hn(element, showClass)) { - showModalEvent.relatedTarget = relatedTarget || void 0; - K(element, showModalEvent); - if (!showModalEvent.defaultPrevented) { - const currentOpen = getCurrentOpen(element); - // istanbul ignore else @preserve - if (currentOpen && currentOpen !== element) { - const that = getModalInstance(currentOpen) || // istanbul ignore next @preserve - Rn( - currentOpen, - offcanvasComponent - ); - // istanbul ignore else @preserve - if (that) that.hide(); - } - if (backdrop) { - if (!hasPopup(overlay)) { - appendOverlay(element, hasFade, true); - } else { - toggleOverlayType(true); - } - overlayDelay = re(overlay); - showOverlay(); - setTimeout(() => beforeModalShow(this), overlayDelay); - } else { - beforeModalShow(this); - // istanbul ignore else @preserve - if (currentOpen && Hn(overlay, showClass)) { - hideOverlay(); - } - } + if (Gn(element, showClass)) return; + showModalEvent.relatedTarget = relatedTarget || void 0; + G(element, showModalEvent); + if (showModalEvent.defaultPrevented) return; + const currentOpen = getCurrentOpen(element); + if (currentOpen && currentOpen !== element) { + const that = getModalInstance(currentOpen) || Xn( + currentOpen, + offcanvasComponent + ); + if (that) that.hide(); + } + if (backdrop) { + if (!hasPopup(overlay)) { + appendOverlay(element, hasFade, true); + } else { + toggleOverlayType(true); + } + overlayDelay = ae(overlay); + showOverlay(); + setTimeout(() => beforeModalShow(this), overlayDelay); + } else { + beforeModalShow(this); + if (currentOpen && Gn(overlay, showClass)) { + hideOverlay(); } } } - /** Hide the modal from the user. */ hide() { const { element, hasFade, relatedTarget } = this; - // istanbul ignore else @preserve - if (Hn(element, showClass)) { - hideModalEvent.relatedTarget = relatedTarget || void 0; - K(element, hideModalEvent); - // istanbul ignore else @preserve - if (!hideModalEvent.defaultPrevented) { - Fn(element, showClass); - In(element, X, "true"); - zn(element, Te); - if (hasFade) { - qn(element, () => beforeModalHide(this)); - } else { - beforeModalHide(this); - } - } - } - } - /** Removes the `Modal` component from target element. */ + if (!Gn(element, showClass)) return; + hideModalEvent.relatedTarget = relatedTarget || void 0; + G(element, hideModalEvent); + if (hideModalEvent.defaultPrevented) return; + qn(element, showClass); + Wn(element, $, "true"); + Qn(element, ze); + if (hasFade) no(element, () => beforeModalHide(this)); + else beforeModalHide(this); + } + update = () => { + if (Gn(this.element, showClass)) setModalScrollbar(this); + }; + _toggleEventListeners = (add) => { + const action = add ? E : r; + const { triggers } = this; + if (!triggers.length) return; + triggers.forEach((btn) => { + action(btn, gt, modalClickHandler); + }); + }; dispose() { const clone = { ...this }; const { modalDialog, hasFade } = clone; @@ -1887,269 +1630,212 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy this.hide(); this._toggleEventListeners(); if (hasFade) { - qn(modalDialog, callback); + no(modalDialog, callback); } else { callback(); } } } - __publicField(Modal, "selector", modalSelector); - __publicField(Modal, "init", modalInitCallback); - __publicField(Modal, "getInstance", getModalInstance); const offcanvasSelector = `.${offcanvasString}`; const offcanvasToggleSelector = `[${dataBsToggle}="${offcanvasString}"]`; const offcanvasDismissSelector = `[${dataBsDismiss}="${offcanvasString}"]`; const offcanvasTogglingClass = `${offcanvasString}-toggling`; const offcanvasDefaults = { backdrop: true, - // boolean keyboard: true, - // boolean scroll: false - // boolean }; - const getOffcanvasInstance = (element) => Rn(element, offcanvasComponent); + const getOffcanvasInstance = (element) => Xn(element, offcanvasComponent); const offcanvasInitCallback = (element) => new Offcanvas(element); - const showOffcanvasEvent = to(`show.bs.${offcanvasString}`); - const shownOffcanvasEvent = to(`shown.bs.${offcanvasString}`); - const hideOffcanvasEvent = to(`hide.bs.${offcanvasString}`); - const hiddenOffcanvasEvent = to(`hidden.bs.${offcanvasString}`); + const showOffcanvasEvent = po(`show.bs.${offcanvasString}`); + const shownOffcanvasEvent = po(`shown.bs.${offcanvasString}`); + const hideOffcanvasEvent = po(`hide.bs.${offcanvasString}`); + const hiddenOffcanvasEvent = po(`hidden.bs.${offcanvasString}`); const setOffCanvasScrollbar = (self) => { const { element } = self; - const { clientHeight, scrollHeight } = k(element); + const { clientHeight, scrollHeight } = w$1(element); setScrollbar(element, clientHeight !== scrollHeight); }; const toggleOffCanvasDismiss = (self, add) => { - const action = add ? E$1 : r; + const action = add ? E : r; const doc = d(self.element); - action(doc, ut, offcanvasKeyDismissHandler); - action(doc, ft, offcanvasDismissHandler); + action(doc, lt, offcanvasKeyDismissHandler); + action(doc, gt, offcanvasDismissHandler); }; const beforeOffcanvasShow = (self) => { const { element, options } = self; - // istanbul ignore else @preserve if (!options.scroll) { setOffCanvasScrollbar(self); - oo(ao(element), { overflow: "hidden" }); + vo(wo(element), { overflow: "hidden" }); } - Bn(element, offcanvasTogglingClass); - Bn(element, showClass); - oo(element, { visibility: "visible" }); - qn(element, () => showOffcanvasComplete(self)); + Kn(element, offcanvasTogglingClass); + Kn(element, showClass); + vo(element, { visibility: "visible" }); + no(element, () => showOffcanvasComplete(self)); }; const beforeOffcanvasHide = (self) => { const { element, options } = self; const currentOpen = getCurrentOpen(element); element.blur(); - if (!currentOpen && options.backdrop && Hn(overlay, showClass)) { + if (!currentOpen && options.backdrop && Gn(overlay, showClass)) { hideOverlay(); } - qn(element, () => hideOffcanvasComplete(self)); + no(element, () => hideOffcanvasComplete(self)); }; - const offcanvasTriggerHandler = (e2) => { - const trigger = Ee(e2.target, offcanvasToggleSelector); - const element = trigger && getTargetElement(trigger); + function offcanvasTriggerHandler(e2) { + const element = getTargetElement(this); const self = element && getOffcanvasInstance(element); - // istanbul ignore else @preserve - if (self) { - self.relatedTarget = trigger; - self.toggle(); - // istanbul ignore else @preserve - if (trigger && trigger.tagName === "A") { - e2.preventDefault(); - } - } - }; + if (isDisabled(this)) return; + if (!self) return; + self.relatedTarget = this; + self.toggle(); + if (this.tagName === "A") e2.preventDefault(); + } const offcanvasDismissHandler = (e2) => { const { target } = e2; - const element = Co( + const element = Ho( offcanvasActiveSelector, d(target) ); - const offCanvasDismiss = Co( + if (!element) return; + const offCanvasDismiss = Ho( offcanvasDismissSelector, element ); - const self = element && getOffcanvasInstance(element); - // istanbul ignore else @preserve - if (self) { - const { options, triggers } = self; - const { backdrop } = options; - const trigger = Ee(target, offcanvasToggleSelector); - const selection = d(element).getSelection(); - // istanbul ignore else: a filter is required here @preserve - if (!overlay.contains(target) || backdrop !== "static") { - // istanbul ignore else @preserve - if (!(selection && selection.toString().length) && (!element.contains(target) && backdrop && // istanbul ignore next @preserve - (!trigger || triggers.includes(target)) || offCanvasDismiss && offCanvasDismiss.contains(target))) { - self.relatedTarget = offCanvasDismiss && offCanvasDismiss.contains(target) ? offCanvasDismiss : null; - self.hide(); - } - // istanbul ignore next @preserve - if (trigger && trigger.tagName === "A") e2.preventDefault(); - } + const self = getOffcanvasInstance(element); + if (!self) return; + const { options, triggers } = self; + const { backdrop } = options; + const trigger = Se(target, offcanvasToggleSelector); + const selection = d(element).getSelection(); + if (overlay.contains(target) && backdrop === "static") return; + if (!(selection && selection.toString().length) && (!element.contains(target) && backdrop && (!trigger || triggers.includes(target)) || offCanvasDismiss && offCanvasDismiss.contains(target))) { + self.relatedTarget = offCanvasDismiss && offCanvasDismiss.contains(target) ? offCanvasDismiss : void 0; + self.hide(); } + if (trigger && trigger.tagName === "A") e2.preventDefault(); }; const offcanvasKeyDismissHandler = ({ code, target }) => { - const element = Co( + const element = Ho( offcanvasActiveSelector, d(target) ); const self = element && getOffcanvasInstance(element); - // istanbul ignore else @preserve - if (self) { - // istanbul ignore else @preserve - if (self.options.keyboard && code === sn) { - self.relatedTarget = null; - self.hide(); - } + if (!self) return; + if (self.options.keyboard && code === fn) { + self.relatedTarget = void 0; + self.hide(); } }; const showOffcanvasComplete = (self) => { const { element } = self; - Fn(element, offcanvasTogglingClass); - zn(element, X); - In(element, Te, "true"); - In(element, "role", "dialog"); - K(element, shownOffcanvasEvent); + qn(element, offcanvasTogglingClass); + Qn(element, $); + Wn(element, ze, "true"); + Wn(element, "role", "dialog"); + G(element, shownOffcanvasEvent); toggleOffCanvasDismiss(self, true); - Jn(element); ro(element); + yo(element); }; const hideOffcanvasComplete = (self) => { const { element, triggers } = self; - In(element, X, "true"); - zn(element, Te); - zn(element, "role"); - oo(element, { visibility: "" }); + Wn(element, $, "true"); + Qn(element, ze); + Qn(element, "role"); + vo(element, { visibility: "" }); const visibleTrigger = showOffcanvasEvent.relatedTarget || triggers.find(isVisible); - // istanbul ignore else @preserve - if (visibleTrigger) Jn(visibleTrigger); + if (visibleTrigger) ro(visibleTrigger); removeOverlay(element); - K(element, hiddenOffcanvasEvent); - Fn(element, offcanvasTogglingClass); - ro(element); + G(element, hiddenOffcanvasEvent); + qn(element, offcanvasTogglingClass); + yo(element); if (!getCurrentOpen(element)) { toggleOffCanvasDismiss(self); } }; class Offcanvas extends BaseComponent { - /** - * @param target usually an `.offcanvas` element - * @param config instance options - */ + static selector = offcanvasSelector; + static init = offcanvasInitCallback; + static getInstance = getOffcanvasInstance; constructor(target, config) { super(target, config); - /** - * Toggles on/off the `click` event listeners. - * - * @param self the `Offcanvas` instance - * @param add when *true*, listeners are added - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - this.triggers.forEach( - (btn) => action(btn, ft, offcanvasTriggerHandler) - ); - }); const { element } = this; this.triggers = [ - ...ue(offcanvasToggleSelector, d(element)) + ...ue( + offcanvasToggleSelector, + d(element) + ) ].filter( (btn) => getTargetElement(btn) === element ); - this.relatedTarget = null; + this.relatedTarget = void 0; this._toggleEventListeners(true); } - /** - * Returns component name string. - */ get name() { return offcanvasComponent; } - /** - * Returns component default options. - */ get defaults() { return offcanvasDefaults; } - // OFFCANVAS PUBLIC METHODS - // ======================== - /** Shows or hides the offcanvas from the user. */ toggle() { - if (Hn(this.element, showClass)) this.hide(); + if (Gn(this.element, showClass)) this.hide(); else this.show(); } - /** Shows the offcanvas to the user. */ show() { const { element, options, relatedTarget } = this; let overlayDelay = 0; - if (!Hn(element, showClass)) { - showOffcanvasEvent.relatedTarget = relatedTarget || void 0; - shownOffcanvasEvent.relatedTarget = relatedTarget || void 0; - K(element, showOffcanvasEvent); - if (!showOffcanvasEvent.defaultPrevented) { - const currentOpen = getCurrentOpen(element); - if (currentOpen && currentOpen !== element) { - const that = getOffcanvasInstance(currentOpen) || // istanbul ignore next @preserve - Rn( - currentOpen, - modalComponent - ); - // istanbul ignore else @preserve - if (that) that.hide(); - } - if (options.backdrop) { - if (!hasPopup(overlay)) { - appendOverlay(element, true); - } else { - toggleOverlayType(); - } - overlayDelay = re(overlay); - showOverlay(); - setTimeout(() => beforeOffcanvasShow(this), overlayDelay); - } else { - beforeOffcanvasShow(this); - // istanbul ignore next @preserve - this test was done on Modal - if (currentOpen && Hn(overlay, showClass)) { - hideOverlay(); - } - } - } + if (Gn(element, showClass)) return; + showOffcanvasEvent.relatedTarget = relatedTarget || void 0; + shownOffcanvasEvent.relatedTarget = relatedTarget || void 0; + G(element, showOffcanvasEvent); + if (showOffcanvasEvent.defaultPrevented) return; + const currentOpen = getCurrentOpen(element); + if (currentOpen && currentOpen !== element) { + const that = getOffcanvasInstance(currentOpen) || Xn( + currentOpen, + modalComponent + ); + if (that) that.hide(); + } + if (options.backdrop) { + if (!hasPopup(overlay)) appendOverlay(element, true); + else toggleOverlayType(); + overlayDelay = ae(overlay); + showOverlay(); + setTimeout(() => beforeOffcanvasShow(this), overlayDelay); + } else { + beforeOffcanvasShow(this); + if (currentOpen && Gn(overlay, showClass)) hideOverlay(); } } - /** Hides the offcanvas from the user. */ hide() { const { element, relatedTarget } = this; - if (Hn(element, showClass)) { - hideOffcanvasEvent.relatedTarget = relatedTarget || void 0; - hiddenOffcanvasEvent.relatedTarget = relatedTarget || void 0; - K(element, hideOffcanvasEvent); - if (!hideOffcanvasEvent.defaultPrevented) { - Bn(element, offcanvasTogglingClass); - Fn(element, showClass); - beforeOffcanvasHide(this); - } - } - } - /** Removes the `Offcanvas` from the target element. */ + if (!Gn(element, showClass)) return; + hideOffcanvasEvent.relatedTarget = relatedTarget || void 0; + hiddenOffcanvasEvent.relatedTarget = relatedTarget || void 0; + G(element, hideOffcanvasEvent); + if (hideOffcanvasEvent.defaultPrevented) return; + Kn(element, offcanvasTogglingClass); + qn(element, showClass); + beforeOffcanvasHide(this); + } + _toggleEventListeners = (add) => { + const action = add ? E : r; + this.triggers.forEach((btn) => { + action(btn, gt, offcanvasTriggerHandler); + }); + }; dispose() { const { element } = this; - const isOpen = Hn(element, showClass); + const isOpen = Gn(element, showClass); const callback = () => setTimeout(() => super.dispose(), 1); this.hide(); this._toggleEventListeners(); - if (isOpen) { - qn(element, callback); - // istanbul ignore next @preserve - } else { - callback(); - } + if (isOpen) no(element, callback); + else callback(); } } - __publicField(Offcanvas, "selector", offcanvasSelector); - __publicField(Offcanvas, "init", offcanvasInitCallback); - __publicField(Offcanvas, "getInstance", getOffcanvasInstance); const popoverString = "popover"; const popoverComponent = "Popover"; const tooltipString = "tooltip"; @@ -2168,25 +1854,23 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy right: "end" }; const styleTip = (self) => { - const tipClasses = /\b(top|bottom|start|end)+/; - const { element, tooltip, container, options, arrow } = self; - // istanbul ignore else @preserve - if (tooltip) { - const tipPositions = { ...tipClassPositions }; - const RTL = To(element); - oo(tooltip, { - // top: '0px', left: '0px', right: '', bottom: '', + requestAnimationFrame(() => { + const tipClasses = /\b(top|bottom|start|end)+/; + const { element, tooltip, container, offsetParent, options, arrow } = self; + if (!tooltip) return; + const RTL = Bo(element); + const { x: scrollLeft, y: scrollTop } = So(offsetParent); + vo(tooltip, { top: "", left: "", right: "", bottom: "" }); - const isPopover = self.name === popoverComponent; const { offsetWidth: tipWidth, offsetHeight: tipHeight } = tooltip; - const { clientWidth: htmlcw, clientHeight: htmlch, offsetWidth: htmlow } = k(element); + const { clientWidth: htmlcw, clientHeight: htmlch, offsetWidth: htmlow } = w$1(element); let { placement } = options; const { clientWidth: parentCWidth, offsetWidth: parentOWidth } = container; - const parentPosition = g( + const parentPosition = f$1( container, "position" ); @@ -2194,18 +1878,23 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const scrollbarWidth = fixedParent ? Math.abs(parentCWidth - parentOWidth) : Math.abs(htmlcw - htmlow); const leftBoundry = RTL && fixedParent ? scrollbarWidth : 0; const rightBoundry = htmlcw - (!RTL ? scrollbarWidth : 0) - 1; + const observerEntry = self._observer.getEntry(element); const { width: elemWidth, height: elemHeight, left: elemRectLeft, right: elemRectRight, top: elemRectTop - } = h(element, true); - const { x: x2, y } = { - x: elemRectLeft, - y: elemRectTop - }; - oo(arrow, { + } = observerEntry?.boundingClientRect || y(element, true); + const { + x: elemOffsetLeft, + y: elemOffsetTop + } = No( + element, + offsetParent, + { x: scrollLeft, y: scrollTop } + ); + vo(arrow, { top: "", left: "", right: "", @@ -2239,58 +1928,56 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy if (!tooltip.className.includes(placement)) { tooltip.className = tooltip.className.replace( tipClasses, - tipPositions[placement] + tipClassPositions[placement] ); } - // istanbul ignore else @preserve if (horizontals.includes(placement)) { if (placement === "left") { - leftPosition = x2 - tipWidth - (isPopover ? arrowWidth : 0); + leftPosition = elemOffsetLeft - tipWidth - arrowWidth; } else { - leftPosition = x2 + elemWidth + (isPopover ? arrowWidth : 0); + leftPosition = elemOffsetLeft + elemWidth + arrowWidth; } if (topExceed && bottomExceed) { topPosition = 0; bottomPosition = 0; - arrowTop = elemRectTop + elemHeight / 2 - arrowHeight / 2; + arrowTop = elemOffsetTop + elemHeight / 2 - arrowHeight / 2; } else if (topExceed) { - topPosition = y; + topPosition = elemOffsetTop; bottomPosition = ""; arrowTop = elemHeight / 2 - arrowWidth; } else if (bottomExceed) { - topPosition = y - tipHeight + elemHeight; + topPosition = elemOffsetTop - tipHeight + elemHeight; bottomPosition = ""; arrowTop = tipHeight - elemHeight / 2 - arrowWidth; } else { - topPosition = y - tipHeight / 2 + elemHeight / 2; + topPosition = elemOffsetTop - tipHeight / 2 + elemHeight / 2; arrowTop = tipHeight / 2 - arrowHeight / 2; } } else if (verticals.includes(placement)) { if (placement === "top") { - topPosition = y - tipHeight - (isPopover ? arrowHeight : 0); + topPosition = elemOffsetTop - tipHeight - arrowHeight; } else { - topPosition = y + elemHeight + (isPopover ? arrowHeight : 0); + topPosition = elemOffsetTop + elemHeight + arrowHeight; } if (leftExceed) { leftPosition = 0; - arrowLeft = x2 + elemWidth / 2 - arrowAdjust; + arrowLeft = elemOffsetLeft + elemWidth / 2 - arrowAdjust; } else if (rightExceed) { leftPosition = "auto"; rightPosition = 0; arrowRight = elemWidth / 2 + rightBoundry - elemRectRight - arrowAdjust; } else { - leftPosition = x2 - tipWidth / 2 + elemWidth / 2; + leftPosition = elemOffsetLeft - tipWidth / 2 + elemWidth / 2; arrowLeft = tipWidth / 2 - arrowAdjust; } } - oo(tooltip, { + vo(tooltip, { top: `${topPosition}px`, bottom: bottomPosition === "" ? "" : `${bottomPosition}px`, left: leftPosition === "auto" ? leftPosition : `${leftPosition}px`, right: rightPosition !== "" ? `${rightPosition}px` : "" }); - // istanbul ignore else @preserve - if (l(arrow)) { + if (b(arrow)) { if (arrowTop !== "") { arrow.style.top = `${arrowTop}px`; } @@ -2300,11 +1987,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy arrow.style.right = `${arrowRight}px`; } } - const updatedTooltipEvent = to( - `updated.bs.${ae(self.name)}` + const updatedTooltipEvent = po( + `updated.bs.${Eo(self.name)}` ); - K(element, updatedTooltipEvent); - } + G(element, updatedTooltipEvent); + }); }; const tooltipDefaults = { template: getTipTemplate(tooltipString), @@ -2323,16 +2010,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const dataOriginalTitle = "data-original-title"; const tooltipComponent = "Tooltip"; const setHtml = (element, content, sanitizeFn) => { - // istanbul ignore else @preserve - if (M(content) && content.length) { + if (N(content) && content.length) { let dirty = content.trim(); - if (ho(sanitizeFn)) dirty = sanitizeFn(dirty); + if (Lo(sanitizeFn)) dirty = sanitizeFn(dirty); const domParser = new DOMParser(); const tempDocument = domParser.parseFromString(dirty, "text/html"); element.append(...[...tempDocument.body.childNodes]); - } else if (l(content)) { + } else if (b(content)) { element.append(content); - } else if (Mo(content) || ge(content) && content.every(i)) { + } else if (Fo(content) || we(content) && content.every(u)) { element.append(...[...content]); } }; @@ -2354,182 +2040,164 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const tipPositions = { ...tipClassPositions }; let titleParts = []; let contentParts = []; - if (To(element)) { + if (Bo(element)) { tipPositions.left = "end"; tipPositions.right = "start"; } const placementClass = `bs-${tipString}-${tipPositions[placement]}`; let tooltipTemplate; - if (l(template)) { + if (b(template)) { tooltipTemplate = template; } else { - const htmlMarkup = ee("div"); + const htmlMarkup = ne("div"); setHtml(htmlMarkup, template, sanitizeFn); tooltipTemplate = htmlMarkup.firstChild; } - self.tooltip = l(tooltipTemplate) ? tooltipTemplate.cloneNode(true) : void 0; + if (!b(tooltipTemplate)) return; + self.tooltip = tooltipTemplate.cloneNode(true); const { tooltip } = self; - // istanbul ignore else @preserve - if (tooltip) { - In(tooltip, "id", id); - In(tooltip, "role", tooltipString); - const bodyClass = isTooltip ? `${tooltipString}-inner` : `${popoverString}-body`; - const tooltipHeader = isTooltip ? null : Co(`.${popoverString}-header`, tooltip); - const tooltipBody = Co(`.${bodyClass}`, tooltip); - self.arrow = Co(`.${tipString}-arrow`, tooltip); - const { arrow } = self; - if (l(title)) titleParts = [title.cloneNode(true)]; - else { - const tempTitle = ee("div"); - setHtml(tempTitle, title, sanitizeFn); - titleParts = [...[...tempTitle.childNodes]]; - } - if (l(content)) contentParts = [content.cloneNode(true)]; - else { - const tempContent = ee("div"); - setHtml(tempContent, content, sanitizeFn); - contentParts = [...[...tempContent.childNodes]]; - } - if (dismissible) { - if (title) { - if (l(btnClose)) { - titleParts = [...titleParts, btnClose.cloneNode(true)]; - } else { - const tempBtn = ee("div"); - setHtml(tempBtn, btnClose, sanitizeFn); - titleParts = [...titleParts, tempBtn.firstChild]; - } + Wn(tooltip, "id", id); + Wn(tooltip, "role", tooltipString); + const bodyClass = isTooltip ? `${tooltipString}-inner` : `${popoverString}-body`; + const tooltipHeader = isTooltip ? null : Ho(`.${popoverString}-header`, tooltip); + const tooltipBody = Ho(`.${bodyClass}`, tooltip); + self.arrow = Ho( + `.${tipString}-arrow`, + tooltip + ); + const { arrow } = self; + if (b(title)) titleParts = [title.cloneNode(true)]; + else { + const tempTitle = ne("div"); + setHtml(tempTitle, title, sanitizeFn); + titleParts = [...[...tempTitle.childNodes]]; + } + if (b(content)) contentParts = [content.cloneNode(true)]; + else { + const tempContent = ne("div"); + setHtml(tempContent, content, sanitizeFn); + contentParts = [...[...tempContent.childNodes]]; + } + if (dismissible) { + if (title) { + if (b(btnClose)) { + titleParts = [...titleParts, btnClose.cloneNode(true)]; } else { - // istanbul ignore else @preserve - if (tooltipHeader) tooltipHeader.remove(); - if (l(btnClose)) { - contentParts = [...contentParts, btnClose.cloneNode(true)]; - } else { - const tempBtn = ee("div"); - setHtml(tempBtn, btnClose, sanitizeFn); - contentParts = [...contentParts, tempBtn.firstChild]; - } - } - } - // istanbul ignore else @preserve - if (!isTooltip) { - // istanbul ignore else @preserve - if (title && tooltipHeader) { - setHtml(tooltipHeader, titleParts, sanitizeFn); + const tempBtn = ne("div"); + setHtml(tempBtn, btnClose, sanitizeFn); + titleParts = [...titleParts, tempBtn.firstChild]; } - // istanbul ignore else @preserve - if (content && tooltipBody) { - setHtml(tooltipBody, contentParts, sanitizeFn); + } else { + if (tooltipHeader) tooltipHeader.remove(); + if (b(btnClose)) { + contentParts = [...contentParts, btnClose.cloneNode(true)]; + } else { + const tempBtn = ne("div"); + setHtml(tempBtn, btnClose, sanitizeFn); + contentParts = [...contentParts, tempBtn.firstChild]; } - self.btn = Co(".btn-close", tooltip) || void 0; - } else if (title && tooltipBody) setHtml(tooltipBody, title, sanitizeFn); - Bn(tooltip, "position-fixed"); - Bn(arrow, "position-absolute"); - // istanbul ignore else @preserve - if (!Hn(tooltip, tipString)) Bn(tooltip, tipString); - // istanbul ignore else @preserve - if (animation && !Hn(tooltip, fadeClass)) { - Bn(tooltip, fadeClass); } - // istanbul ignore else @preserve - if (customClass && !Hn(tooltip, customClass)) { - Bn(tooltip, customClass); + } + if (!isTooltip) { + if (title && tooltipHeader) { + setHtml(tooltipHeader, titleParts, sanitizeFn); + } + if (content && tooltipBody) { + setHtml(tooltipBody, contentParts, sanitizeFn); } - // istanbul ignore else @preserve - if (!Hn(tooltip, placementClass)) Bn(tooltip, placementClass); + self.btn = Ho(".btn-close", tooltip) || void 0; + } else if (title && tooltipBody) setHtml(tooltipBody, title, sanitizeFn); + Kn(tooltip, "position-absolute"); + Kn(arrow, "position-absolute"); + if (!Gn(tooltip, tipString)) Kn(tooltip, tipString); + if (animation && !Gn(tooltip, fadeClass)) { + Kn(tooltip, fadeClass); + } + if (customClass && !Gn(tooltip, customClass)) { + Kn(tooltip, customClass); } + if (!Gn(tooltip, placementClass)) Kn(tooltip, placementClass); }; const getElementContainer = (element) => { const majorBlockTags = ["HTML", "BODY"]; const containers = []; let { parentNode } = element; while (parentNode && !majorBlockTags.includes(parentNode.nodeName)) { - parentNode = lo(parentNode); - // istanbul ignore else @preserve - if (!(pe(parentNode) || Do(parentNode))) { + parentNode = k$1(parentNode); + if (!(pe(parentNode) || me(parentNode))) { containers.push(parentNode); } } return containers.find((c, i2) => { - if (g(c, "position") !== "relative" && containers.slice(i2 + 1).every( - (r2) => g(r2, "position") === "static" + if ((f$1(c, "position") !== "relative" || f$1(c, "position") === "relative" && c.offsetHeight !== c.scrollHeight) && containers.slice(i2 + 1).every( + (r2) => f$1(r2, "position") === "static" )) { return c; } return null; - }) || // istanbul ignore next: optional guard - d(element).body; + }) || d(element).body; }; const tooltipSelector = `[${dataBsToggle}="${tooltipString}"],[data-tip="${tooltipString}"]`; const titleAttr = "title"; - let getTooltipInstance = (element) => Rn(element, tooltipComponent); + let getTooltipInstance = (element) => Xn(element, tooltipComponent); const tooltipInitCallback = (element) => new Tooltip(element); const removeTooltip = (self) => { - const { element, tooltip, container, offsetParent } = self; - zn(element, we); + const { element, tooltip, container } = self; + Qn(element, Me); removePopup( tooltip, - container === offsetParent ? container : offsetParent + container ); }; const hasTip = (self) => { - const { tooltip, container, offsetParent } = self; - return tooltip && hasPopup(tooltip, container === offsetParent ? container : offsetParent); + const { tooltip, container } = self; + return tooltip && hasPopup(tooltip, container); }; const disposeTooltipComplete = (self, callback) => { const { element } = self; self._toggleEventListeners(); - // istanbul ignore else @preserve - if (te(element, dataOriginalTitle) && self.name === tooltipComponent) { + if (ee(element, dataOriginalTitle) && self.name === tooltipComponent) { toggleTooltipTitle(self); } - // istanbul ignore else @preserve if (callback) callback(); }; const toggleTooltipAction = (self, add) => { - const action = add ? E$1 : r; + const action = add ? E : r; const { element } = self; action( d(element), - Ut, + Wt, self.handleTouch, - eo + go ); - [Ht, zt].forEach((ev) => { - action(fo(element), ev, self.update, eo); - }); }; const tooltipShownAction = (self) => { const { element } = self; - const shownTooltipEvent = to( - `shown.bs.${ae(self.name)}` + const shownTooltipEvent = po( + `shown.bs.${Eo(self.name)}` ); toggleTooltipAction(self, true); - K(element, shownTooltipEvent); - so.clear(element, "in"); + G(element, shownTooltipEvent); + bo.clear(element, "in"); }; const tooltipHiddenAction = (self) => { const { element } = self; - const hiddenTooltipEvent = to( - `hidden.bs.${ae(self.name)}` + const hiddenTooltipEvent = po( + `hidden.bs.${Eo(self.name)}` ); toggleTooltipAction(self); removeTooltip(self); - K(element, hiddenTooltipEvent); - so.clear(element, "out"); + G(element, hiddenTooltipEvent); + bo.clear(element, "out"); }; const toggleTooltipOpenHandlers = (self, add) => { - const action = add ? E$1 : r; - const { element, container, offsetParent } = self; - const { offsetHeight, scrollHeight } = container; - const parentModal = Ee(element, `.${modalString}`); - const parentOffcanvas = Ee(element, `.${offcanvasString}`); - // istanbul ignore else @preserve - const win = fo(element); - const overflow = offsetHeight !== scrollHeight; - const scrollTarget = container === offsetParent && overflow ? container : win; - action(scrollTarget, zt, self.update, eo); - action(scrollTarget, Ht, self.update, eo); + const action = add ? E : r; + const { element, tooltip } = self; + const parentModal = Se(element, `.${modalString}`); + const parentOffcanvas = Se(element, `.${offcanvasString}`); + if (add) { + [element, tooltip].forEach((target) => self._observer.observe(target)); + } else self._observer.disconnect(); if (parentModal) { action(parentModal, `hide.bs.${modalString}`, self.handleHide); } @@ -2540,162 +2208,75 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const toggleTooltipTitle = (self, content) => { const titleAtt = [dataOriginalTitle, titleAttr]; const { element } = self; - In( + Wn( element, titleAtt[content ? 0 : 1], - content || j(element, titleAtt[0]) || // istanbul ignore next @preserve - "" + content || j(element, titleAtt[0]) || "" ); - zn(element, titleAtt[content ? 1 : 0]); + Qn(element, titleAtt[content ? 1 : 0]); }; class Tooltip extends BaseComponent { - /** - * @param target the target element - * @param config the instance options - */ + static selector = tooltipSelector; + static init = tooltipInitCallback; + static getInstance = getTooltipInstance; + static styleTip = styleTip; constructor(target, config) { super(target, config); - // TOOLTIP PUBLIC METHODS - // ====================== - /** Handles the focus event on iOS. */ - // istanbul ignore next @preserve - impossible to test without Apple device - __publicField(this, "handleFocus", () => Jn(this.element)); - /** Shows the tooltip. */ - __publicField(this, "handleShow", () => this.show()); - /** Hides the tooltip. */ - __publicField(this, "handleHide", () => this.hide()); - /** Updates the tooltip position. */ - __publicField(this, "update", () => { - styleTip(this); - }); - /** Toggles the tooltip visibility. */ - __publicField(this, "toggle", () => { - const { tooltip } = this; - if (tooltip && !hasTip(this)) this.show(); - else this.hide(); - }); - /** - * Handles the `touchstart` event listener for `Tooltip` - * - * @this {Tooltip} - * @param {TouchEvent} e the `Event` object - */ - __publicField(this, "handleTouch", ({ target }) => { - const { tooltip, element } = this; - // istanbul ignore if @preserve - if (tooltip && tooltip.contains(target) || target === element || target && element.contains(target)) ; - else { - this.hide(); - } - }); - /** - * Toggles on/off the `Tooltip` event listeners. - * - * @param add when `true`, event listeners are added - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - const { element, options, btn } = this; - const { trigger } = options; - const isPopover = this.name !== tooltipComponent; - const dismissible = isPopover && options.dismissible ? true : false; - // istanbul ignore else @preserve - if (!trigger.includes("manual")) { - this.enabled = !!add; - const triggerOptions = trigger.split(" "); - triggerOptions.forEach((tr) => { - // istanbul ignore else @preserve - if (tr === Et) { - action(element, gt, this.handleShow); - action(element, bt, this.handleShow); - // istanbul ignore else @preserve - if (!dismissible) { - action(element, ht, this.handleHide); - action( - d(element), - Ut, - this.handleTouch, - eo - ); - } - } else if (tr === ft) { - action(element, tr, !dismissible ? this.toggle : this.handleShow); - } else if (tr === ot) { - action(element, st, this.handleShow); - // istanbul ignore else @preserve - if (!dismissible) action(element, ct, this.handleHide); - // istanbul ignore else @preserve - if (An) { - action(element, ft, this.handleFocus); - } - } - // istanbul ignore else @preserve - if (dismissible && btn) { - action(btn, ft, this.handleHide); - } - }); - } - }); const { element } = this; const isTooltip = this.name === tooltipComponent; const tipString = isTooltip ? tooltipString : popoverString; const tipComponent = isTooltip ? tooltipComponent : popoverComponent; - // istanbul ignore next @preserve: this is to set Popover too - getTooltipInstance = (elem) => Rn(elem, tipComponent); + getTooltipInstance = (elem) => Xn(elem, tipComponent); this.enabled = true; - this.id = `${tipString}-${me(element, tipString)}`; + this.id = `${tipString}-${ye(element, tipString)}`; const { options } = this; - if (!(!options.title && isTooltip || !isTooltip && !options.content)) { - T(tooltipDefaults, { titleAttr: "" }); - // istanbul ignore else @preserve - if (te(element, titleAttr) && isTooltip && typeof options.title === "string") { - toggleTooltipTitle(this, options.title); - } - this.container = getElementContainer(element); - this.offsetParent = ["sticky", "fixed"].some( - (position) => g(this.container, "position") === position - ) ? this.container : d(this.element).body; - createTip(this); - this._toggleEventListeners(true); - } + if (!options.title && isTooltip || !isTooltip && !options.content) { + return; + } + C(tooltipDefaults, { titleAttr: "" }); + if (ee(element, titleAttr) && isTooltip && typeof options.title === "string") { + toggleTooltipTitle(this, options.title); + } + const container = getElementContainer(element); + const offsetParent = ["sticky", "fixed", "relative"].some( + (position) => f$1(container, "position") === position + ) ? container : ge(element); + this.container = container; + this.offsetParent = offsetParent; + createTip(this); + if (!this.tooltip) return; + this._observer = new v(() => this.update()); + this._toggleEventListeners(true); } - /** - * Returns component name string. - */ get name() { return tooltipComponent; } - /** - * Returns component default options. - */ get defaults() { return tooltipDefaults; } + handleFocus = () => ro(this.element); + handleShow = () => this.show(); show() { - const { options, tooltip, element, container, offsetParent, id } = this; + const { options, tooltip, element, container, id } = this; const { animation } = options; - const outTimer = so.get(element, "out"); - const tipContainer = container === offsetParent ? container : offsetParent; - so.clear(element, "out"); + const outTimer = bo.get(element, "out"); + bo.clear(element, "out"); if (tooltip && !outTimer && !hasTip(this)) { - so.set( + bo.set( element, () => { - const showTooltipEvent = to( - `show.bs.${ae(this.name)}` + const showTooltipEvent = po( + `show.bs.${Eo(this.name)}` ); - K(element, showTooltipEvent); - // istanbul ignore else @preserve + G(element, showTooltipEvent); if (!showTooltipEvent.defaultPrevented) { - appendPopup(tooltip, tipContainer); - In(element, we, `#${id}`); + appendPopup(tooltip, container); + Wn(element, Me, `#${id}`); this.update(); toggleTooltipOpenHandlers(this, true); - // istanbul ignore else @preserve - if (!Hn(tooltip, showClass)) Bn(tooltip, showClass); - // istanbul ignore else @preserve + if (!Gn(tooltip, showClass)) Kn(tooltip, showClass); if (animation) { - qn(tooltip, () => tooltipShownAction(this)); + no(tooltip, () => tooltipShownAction(this)); } else tooltipShownAction(this); } }, @@ -2704,27 +2285,25 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ); } } + handleHide = () => this.hide(); hide() { const { options, tooltip, element } = this; const { animation, delay } = options; - so.clear(element, "in"); - // istanbul ignore else @preserve + bo.clear(element, "in"); if (tooltip && hasTip(this)) { - so.set( + bo.set( element, () => { - const hideTooltipEvent = to( - `hide.bs.${ae(this.name)}` + const hideTooltipEvent = po( + `hide.bs.${Eo(this.name)}` ); - K(element, hideTooltipEvent); - // istanbul ignore else @preserve + G(element, hideTooltipEvent); if (!hideTooltipEvent.defaultPrevented) { this.update(); - Fn(tooltip, showClass); + qn(tooltip, showClass); toggleTooltipOpenHandlers(this); - // istanbul ignore else @preserve if (animation) { - qn(tooltip, () => tooltipHiddenAction(this)); + no(tooltip, () => tooltipHiddenAction(this)); } else tooltipHiddenAction(this); } }, @@ -2733,31 +2312,77 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ); } } - /** Enables the tooltip. */ + update = () => { + styleTip(this); + }; + toggle = () => { + const { tooltip } = this; + if (tooltip && !hasTip(this)) this.show(); + else this.hide(); + }; enable() { const { enabled } = this; - // istanbul ignore else @preserve if (!enabled) { this._toggleEventListeners(true); this.enabled = !enabled; } } - /** Disables the tooltip. */ disable() { const { tooltip, enabled } = this; - // istanbul ignore else @preserve if (enabled) { if (tooltip && hasTip(this)) this.hide(); this._toggleEventListeners(); this.enabled = !enabled; } } - /** Toggles the `disabled` property. */ toggleEnabled() { if (!this.enabled) this.enable(); else this.disable(); } - /** Removes the `Tooltip` from the target element. */ + handleTouch = ({ target }) => { + const { tooltip, element } = this; + if (tooltip && tooltip.contains(target) || target === element || target && element.contains(target)) ; + else { + this.hide(); + } + }; + _toggleEventListeners = (add) => { + const action = add ? E : r; + const { element, options, btn } = this; + const { trigger } = options; + const isPopover = this.name !== tooltipComponent; + const dismissible = isPopover && options.dismissible ? true : false; + if (!trigger.includes("manual")) { + this.enabled = !!add; + const triggerOptions = trigger.split(" "); + triggerOptions.forEach((tr) => { + if (tr === Et) { + action(element, vt, this.handleShow); + action(element, ht, this.handleShow); + if (!dismissible) { + action(element, yt, this.handleHide); + action( + d(element), + Wt, + this.handleTouch, + go + ); + } + } else if (tr === gt) { + action(element, tr, !dismissible ? this.toggle : this.handleShow); + } else if (tr === st) { + action(element, rt, this.handleShow); + if (!dismissible) action(element, ct, this.handleHide); + if (On()) { + action(element, gt, this.handleFocus); + } + } + if (dismissible && btn) { + action(btn, gt, this.handleHide); + } + }); + } + }; dispose() { const { tooltip, options } = this; const clone = { ...this, name: this.name }; @@ -2768,301 +2393,244 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy if (options.animation && hasTip(clone)) { this.options.delay = 0; this.hide(); - qn(tooltip, callback); + no(tooltip, callback); } else { callback(); } } } - __publicField(Tooltip, "selector", tooltipSelector); - __publicField(Tooltip, "init", tooltipInitCallback); - __publicField(Tooltip, "getInstance", getTooltipInstance); - __publicField(Tooltip, "styleTip", styleTip); const popoverSelector = `[${dataBsToggle}="${popoverString}"],[data-tip="${popoverString}"]`; - const popoverDefaults = T({}, tooltipDefaults, { + const popoverDefaults = C({}, tooltipDefaults, { template: getTipTemplate(popoverString), content: "", dismissible: false, - btnClose: '' + btnClose: '' }); - const getPopoverInstance = (element) => Rn(element, popoverComponent); + const getPopoverInstance = (element) => Xn(element, popoverComponent); const popoverInitCallback = (element) => new Popover(element); class Popover extends Tooltip { - /** - * @param target the target element - * @param config the instance options - */ + static selector = popoverSelector; + static init = popoverInitCallback; + static getInstance = getPopoverInstance; + static styleTip = styleTip; constructor(target, config) { super(target, config); - /* extend original `show()` */ - __publicField(this, "show", () => { - super.show(); - const { options, btn } = this; - // istanbul ignore else @preserve - if (options.dismissible && btn) setTimeout(() => Jn(btn), 17); - }); } - /** - * Returns component name string. - */ get name() { return popoverComponent; } - /** - * Returns component default options. - */ get defaults() { return popoverDefaults; } + show = () => { + super.show(); + const { options, btn } = this; + if (options.dismissible && btn) setTimeout(() => ro(btn), 17); + }; } - __publicField(Popover, "selector", popoverSelector); - __publicField(Popover, "init", popoverInitCallback); - __publicField(Popover, "getInstance", getPopoverInstance); - __publicField(Popover, "styleTip", styleTip); const tabString = "tab"; const tabComponent = "Tab"; const tabSelector = `[${dataBsToggle}="${tabString}"]`; - const getTabInstance = (element) => Rn(element, tabComponent); + const getTabInstance = (element) => Xn(element, tabComponent); const tabInitCallback = (element) => new Tab(element); - const showTabEvent = to( + const showTabEvent = po( `show.bs.${tabString}` ); - const shownTabEvent = to( + const shownTabEvent = po( `shown.bs.${tabString}` ); - const hideTabEvent = to( + const hideTabEvent = po( `hide.bs.${tabString}` ); - const hiddenTabEvent = to( + const hiddenTabEvent = po( `hidden.bs.${tabString}` ); const tabPrivate = /* @__PURE__ */ new Map(); const triggerTabEnd = (self) => { const { tabContent, nav } = self; - // istanbul ignore else @preserve - if (tabContent && Hn(tabContent, collapsingClass)) { + if (tabContent && Gn(tabContent, collapsingClass)) { tabContent.style.height = ""; - Fn(tabContent, collapsingClass); + qn(tabContent, collapsingClass); } - // istanbul ignore else @preserve - if (nav) so.clear(nav); + if (nav) bo.clear(nav); }; const triggerTabShow = (self) => { const { element, tabContent, content: nextContent, nav } = self; - const { tab } = l(nav) && tabPrivate.get(nav) || // istanbul ignore next @preserve - { tab: null }; - // istanbul ignore else @preserve - if (tabContent && nextContent && Hn(nextContent, fadeClass)) { - const { currentHeight, nextHeight } = tabPrivate.get(element) || // istanbul ignore next @preserve - { currentHeight: 0, nextHeight: 0 }; - // istanbul ignore else @preserve: vitest won't validate this branch + const { tab } = b(nav) && tabPrivate.get(nav) || { tab: null }; + if (tabContent && nextContent && Gn(nextContent, fadeClass)) { + const { currentHeight, nextHeight } = tabPrivate.get(element) || { currentHeight: 0, nextHeight: 0 }; if (currentHeight !== nextHeight) { setTimeout(() => { tabContent.style.height = `${nextHeight}px`; - no(tabContent); - qn(tabContent, () => triggerTabEnd(self)); + mo(tabContent); + no(tabContent, () => triggerTabEnd(self)); }, 50); } else { triggerTabEnd(self); } - } else if (nav) so.clear(nav); + } else if (nav) bo.clear(nav); shownTabEvent.relatedTarget = tab; - K(element, shownTabEvent); + G(element, shownTabEvent); }; const triggerTabHide = (self) => { const { element, content: nextContent, tabContent, nav } = self; - const { tab, content } = nav && tabPrivate.get(nav) || // istanbul ignore next @preserve - { tab: null, content: null }; + const { tab, content } = nav && tabPrivate.get(nav) || { tab: null, content: null }; let currentHeight = 0; - // istanbul ignore else @preserve - if (tabContent && nextContent && Hn(nextContent, fadeClass)) { + if (tabContent && nextContent && Gn(nextContent, fadeClass)) { [content, nextContent].forEach((c) => { - // istanbul ignore else @preserve - if (l(c)) Bn(c, "overflow-hidden"); + if (c) Kn(c, "overflow-hidden"); }); - currentHeight = l(content) ? content.scrollHeight : 0; + currentHeight = content ? content.scrollHeight : 0; } showTabEvent.relatedTarget = tab; hiddenTabEvent.relatedTarget = element; - K(element, showTabEvent); - // istanbul ignore else @preserve - if (!showTabEvent.defaultPrevented) { - // istanbul ignore else @preserve - if (nextContent) Bn(nextContent, activeClass); - // istanbul ignore else @preserve - if (content) Fn(content, activeClass); - // istanbul ignore else @preserve - if (tabContent && nextContent && Hn(nextContent, fadeClass)) { - const nextHeight = nextContent.scrollHeight; - tabPrivate.set(element, { - currentHeight, - nextHeight, - tab: null, - content: null - }); - Bn(tabContent, collapsingClass); - tabContent.style.height = `${currentHeight}px`; - no(tabContent); - [content, nextContent].forEach((c) => { - // istanbul ignore else @preserve - if (c) Fn(c, "overflow-hidden"); + G(element, showTabEvent); + if (showTabEvent.defaultPrevented) return; + if (nextContent) Kn(nextContent, activeClass); + if (content) qn(content, activeClass); + if (tabContent && nextContent && Gn(nextContent, fadeClass)) { + const nextHeight = nextContent.scrollHeight; + tabPrivate.set(element, { + currentHeight, + nextHeight, + tab: null, + content: null + }); + Kn(tabContent, collapsingClass); + tabContent.style.height = `${currentHeight}px`; + mo(tabContent); + [content, nextContent].forEach((c) => { + if (c) qn(c, "overflow-hidden"); + }); + } + if (nextContent && nextContent && Gn(nextContent, fadeClass)) { + setTimeout(() => { + Kn(nextContent, showClass); + no(nextContent, () => { + triggerTabShow(self); }); - } - if (nextContent && nextContent && Hn(nextContent, fadeClass)) { - setTimeout(() => { - Bn(nextContent, showClass); - qn(nextContent, () => { - triggerTabShow(self); - }); - }, 1); - } else { - // istanbul ignore else @preserve - if (nextContent) Bn(nextContent, showClass); - triggerTabShow(self); - } - // istanbul ignore else @preserve - if (tab) K(tab, hiddenTabEvent); + }, 1); + } else { + if (nextContent) Kn(nextContent, showClass); + triggerTabShow(self); } + if (tab) G(tab, hiddenTabEvent); }; const getActiveTab = (self) => { const { nav } = self; - // istanbul ignore next @preserve - if (!l(nav)) { + if (!b(nav)) { return { tab: null, content: null }; } - const activeTabs = Io(activeClass, nav); + const activeTabs = Ro( + activeClass, + nav + ); let tab = null; - // istanbul ignore else @preserve if (activeTabs.length === 1 && !dropdownMenuClasses.some( - (c) => Hn(activeTabs[0].parentElement, c) + (c) => Gn(activeTabs[0].parentElement, c) )) { [tab] = activeTabs; } else if (activeTabs.length > 1) { tab = activeTabs[activeTabs.length - 1]; } - const content = l(tab) ? getTargetElement(tab) : null; + const content = b(tab) ? getTargetElement(tab) : null; return { tab, content }; }; const getParentDropdown = (element) => { - // istanbul ignore next @preserve - if (!l(element)) return null; - const dropdown = Ee(element, `.${dropdownMenuClasses.join(",.")}`); - return dropdown ? Co(`.${dropdownMenuClasses[0]}-toggle`, dropdown) : null; + if (!b(element)) return null; + const dropdown = Se(element, `.${dropdownMenuClasses.join(",.")}`); + return dropdown ? Ho(`.${dropdownMenuClasses[0]}-toggle`, dropdown) : null; }; const tabClickHandler = (e2) => { - const self = getTabInstance(e2.target); - // istanbul ignore else @preserve - if (self) { - e2.preventDefault(); - self.show(); - } + const element = Se(e2.target, tabSelector); + const self = element && getTabInstance(element); + if (!self) return; + e2.preventDefault(); + self.show(); }; class Tab extends BaseComponent { - /** @param target the target element */ + static selector = tabSelector; + static init = tabInitCallback; + static getInstance = getTabInstance; constructor(target) { super(target); - /** - * Toggles on/off the `click` event listener. - * - * @param add when `true`, event listener is added - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - action(this.element, ft, tabClickHandler); - }); const { element } = this; const content = getTargetElement(element); - // istanbul ignore else @preserve - if (content) { - const nav = Ee(element, ".nav"); - const container = Ee(content, ".tab-content"); - this.nav = nav; - this.content = content; - this.tabContent = container; - this.dropdown = getParentDropdown(element); - const { tab } = getActiveTab(this); - if (nav && !tab) { - const firstTab = Co(tabSelector, nav); - const firstTabContent = firstTab && getTargetElement(firstTab); - // istanbul ignore else @preserve - if (firstTabContent) { - Bn(firstTab, activeClass); - Bn(firstTabContent, showClass); - Bn(firstTabContent, activeClass); - In(element, De, "true"); - } + if (!content) return; + const nav = Se(element, ".nav"); + const container = Se( + content, + ".tab-content" + ); + this.nav = nav; + this.content = content; + this.tabContent = container; + this.dropdown = getParentDropdown(element); + const { tab } = getActiveTab(this); + if (nav && !tab) { + const firstTab = Ho(tabSelector, nav); + const firstTabContent = firstTab && getTargetElement(firstTab); + if (firstTabContent) { + Kn(firstTab, activeClass); + Kn(firstTabContent, showClass); + Kn(firstTabContent, activeClass); + Wn(element, Pe, "true"); } - this._toggleEventListeners(true); } + this._toggleEventListeners(true); } - /** - * Returns component name string. - */ get name() { return tabComponent; } - // TAB PUBLIC METHODS - // ================== - /** Shows the tab to the user. */ show() { const { element, content: nextContent, nav, dropdown } = this; - // istanbul ignore else @preserve - if (!(nav && so.get(nav)) && !Hn(element, activeClass)) { - const { tab, content } = getActiveTab(this); - // istanbul ignore else @preserve - if (nav) { - tabPrivate.set(nav, { tab, content, currentHeight: 0, nextHeight: 0 }); - } - hideTabEvent.relatedTarget = element; - // istanbul ignore else @preserve - if (l(tab)) { - K(tab, hideTabEvent); - // istanbul ignore else @preserve - if (!hideTabEvent.defaultPrevented) { - Bn(element, activeClass); - In(element, De, "true"); - const activeDropdown = l(tab) && getParentDropdown(tab); - if (activeDropdown && Hn(activeDropdown, activeClass)) { - Fn(activeDropdown, activeClass); - } - // istanbul ignore else @preserve - if (nav) { - const toggleTab = () => { - // istanbul ignore else @preserve - if (tab) { - Fn(tab, activeClass); - In(tab, De, "false"); - } - if (dropdown && !Hn(dropdown, activeClass)) { - Bn(dropdown, activeClass); - } - }; - if (content && (Hn(content, fadeClass) || nextContent && Hn(nextContent, fadeClass))) { - so.set(nav, toggleTab, 1); - } else toggleTab(); - } - // istanbul ignore else @preserve - if (content) { - Fn(content, showClass); - if (Hn(content, fadeClass)) { - qn(content, () => triggerTabHide(this)); - } else { - triggerTabHide(this); - } - } + if (nav && bo.get(nav) || Gn(element, activeClass)) return; + const { tab, content } = getActiveTab(this); + if (nav && tab) { + tabPrivate.set(nav, { tab, content, currentHeight: 0, nextHeight: 0 }); + } + hideTabEvent.relatedTarget = element; + if (!b(tab)) return; + G(tab, hideTabEvent); + if (hideTabEvent.defaultPrevented) return; + Kn(element, activeClass); + Wn(element, Pe, "true"); + const activeDropdown = b(tab) && getParentDropdown(tab); + if (activeDropdown && Gn(activeDropdown, activeClass)) { + qn(activeDropdown, activeClass); + } + if (nav) { + const toggleTab = () => { + if (tab) { + qn(tab, activeClass); + Wn(tab, Pe, "false"); + } + if (dropdown && !Gn(dropdown, activeClass)) { + Kn(dropdown, activeClass); } + }; + if (content && (Gn(content, fadeClass) || nextContent && Gn(nextContent, fadeClass))) { + bo.set(nav, toggleTab, 1); + } else toggleTab(); + } + if (content) { + qn(content, showClass); + if (Gn(content, fadeClass)) { + no(content, () => triggerTabHide(this)); + } else { + triggerTabHide(this); } } } - /** Removes the `Tab` component from the target element. */ + _toggleEventListeners = (add) => { + const action = add ? E : r; + action(this.element, gt, tabClickHandler); + }; dispose() { this._toggleEventListeners(); super.dispose(); } } - __publicField(Tab, "selector", tabSelector); - __publicField(Tab, "init", tabInitCallback); - __publicField(Tab, "getInstance", getTabInstance); const toastString = "toast"; const toastComponent = "Toast"; const toastSelector = `.${toastString}`; @@ -3075,59 +2643,58 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy autohide: true, delay: 5e3 }; - const getToastInstance = (element) => Rn(element, toastComponent); + const getToastInstance = (element) => Xn(element, toastComponent); const toastInitCallback = (element) => new Toast(element); - const showToastEvent = to( + const showToastEvent = po( `show.bs.${toastString}` ); - const shownToastEvent = to( + const shownToastEvent = po( `shown.bs.${toastString}` ); - const hideToastEvent = to( + const hideToastEvent = po( `hide.bs.${toastString}` ); - const hiddenToastEvent = to( + const hiddenToastEvent = po( `hidden.bs.${toastString}` ); const showToastComplete = (self) => { const { element, options } = self; - Fn(element, showingClass); - so.clear(element, showingClass); - K(element, shownToastEvent); - // istanbul ignore else @preserve + qn(element, showingClass); + bo.clear(element, showingClass); + G(element, shownToastEvent); if (options.autohide) { - so.set(element, () => self.hide(), options.delay, toastString); + bo.set(element, () => self.hide(), options.delay, toastString); } }; const hideToastComplete = (self) => { const { element } = self; - Fn(element, showingClass); - Fn(element, showClass); - Bn(element, hideClass); - so.clear(element, toastString); - K(element, hiddenToastEvent); + qn(element, showingClass); + qn(element, showClass); + Kn(element, hideClass); + bo.clear(element, toastString); + G(element, hiddenToastEvent); }; const hideToast = (self) => { const { element, options } = self; - Bn(element, showingClass); + Kn(element, showingClass); if (options.animation) { - no(element); - qn(element, () => hideToastComplete(self)); + mo(element); + no(element, () => hideToastComplete(self)); } else { hideToastComplete(self); } }; const showToast = (self) => { const { element, options } = self; - so.set( + bo.set( element, () => { - Fn(element, hideClass); - no(element); - Bn(element, showClass); - Bn(element, showingClass); + qn(element, hideClass); + mo(element); + Kn(element, showClass); + Kn(element, showingClass); if (options.animation) { - qn(element, () => showToastComplete(self)); + no(element, () => showToastComplete(self)); } else { showToastComplete(self); } @@ -3136,135 +2703,97 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy showingClass ); }; - const toastClickHandler = (e2) => { - const { target } = e2; - const trigger = target && Ee(target, toastToggleSelector); - const element = trigger && getTargetElement(trigger); + function toastClickHandler(e2) { + const element = getTargetElement(this); const self = element && getToastInstance(element); - // istanbul ignore else @preserve - if (self) { - // istanbul ignore else @preserve - if (trigger && trigger.tagName === "A") e2.preventDefault(); - self.relatedTarget = trigger; - self.show(); - } - }; + if (isDisabled(this)) return; + if (!self) return; + if (this.tagName === "A") e2.preventDefault(); + self.relatedTarget = this; + self.show(); + } const interactiveToastHandler = (e2) => { const element = e2.target; const self = getToastInstance(element); const { type, relatedTarget } = e2; - // istanbul ignore else @preserve: a solid filter is required - if (self && element !== relatedTarget && !element.contains(relatedTarget)) { - if ([bt, st].includes(type)) { - so.clear(element, toastString); - } else { - so.set(element, () => self.hide(), self.options.delay, toastString); - } + if (!self || element === relatedTarget || element.contains(relatedTarget)) return; + if ([ht, rt].includes(type)) { + bo.clear(element, toastString); + } else { + bo.set(element, () => self.hide(), self.options.delay, toastString); } }; class Toast extends BaseComponent { - /** - * @param target the target `.toast` element - * @param config the instance options - */ + static selector = toastSelector; + static init = toastInitCallback; + static getInstance = getToastInstance; constructor(target, config) { super(target, config); - // TOAST PUBLIC METHODS - // ==================== - /** Shows the toast. */ - __publicField(this, "show", () => { - const { element, isShown } = this; - // istanbul ignore else @preserve - if (element && !isShown) { - K(element, showToastEvent); - if (!showToastEvent.defaultPrevented) { - showToast(this); - } - } - }); - /** Hides the toast. */ - __publicField(this, "hide", () => { - const { element, isShown } = this; - // istanbul ignore else @preserve - if (element && isShown) { - K(element, hideToastEvent); - if (!hideToastEvent.defaultPrevented) { - hideToast(this); - } - } - }); - /** - * Toggles on/off the `click` event listener. - * - * @param add when `true`, it will add the listener - */ - __publicField(this, "_toggleEventListeners", (add) => { - const action = add ? E$1 : r; - const { element, triggers, dismiss, options, hide } = this; - // istanbul ignore else @preserve - if (dismiss) { - action(dismiss, ft, hide); - } - // istanbul ignore else @preserve - if (options.autohide) { - [st, ct, bt, ht].forEach( - (e2) => action(element, e2, interactiveToastHandler) - ); - } - // istanbul ignore else @preserve - if (triggers.length) { - triggers.forEach( - (btn) => action(btn, ft, toastClickHandler) - ); - } - }); const { element, options } = this; - if (options.animation && !Hn(element, fadeClass)) { - Bn(element, fadeClass); - } else if (!options.animation && Hn(element, fadeClass)) { - Fn(element, fadeClass); + if (options.animation && !Gn(element, fadeClass)) { + Kn(element, fadeClass); + } else if (!options.animation && Gn(element, fadeClass)) { + qn(element, fadeClass); } - this.dismiss = Co(toastDismissSelector, element); + this.dismiss = Ho(toastDismissSelector, element); this.triggers = [ - ...ue(toastToggleSelector, d(element)) + ...ue( + toastToggleSelector, + d(element) + ) ].filter( (btn) => getTargetElement(btn) === element ); this._toggleEventListeners(true); } - /** - * Returns component name string. - */ get name() { return toastComponent; } - /** - * Returns component default options. - */ get defaults() { return toastDefaults; } - /** - * Returns *true* when toast is visible. - */ get isShown() { - return Hn(this.element, showClass); + return Gn(this.element, showClass); } - /** Removes the `Toast` component from the target element. */ + show = () => { + const { element, isShown } = this; + if (!element || isShown) return; + G(element, showToastEvent); + if (!showToastEvent.defaultPrevented) showToast(this); + }; + hide = () => { + const { element, isShown } = this; + if (!element || !isShown) return; + G(element, hideToastEvent); + if (!hideToastEvent.defaultPrevented) hideToast(this); + }; + _toggleEventListeners = (add) => { + const action = add ? E : r; + const { element, triggers, dismiss, options, hide } = this; + if (dismiss) { + action(dismiss, gt, hide); + } + if (options.autohide) { + [rt, ct, ht, yt].forEach( + (e2) => action(element, e2, interactiveToastHandler) + ); + } + if (triggers.length) { + triggers.forEach((btn) => { + action(btn, gt, toastClickHandler); + }); + } + }; dispose() { const { element, isShown } = this; this._toggleEventListeners(); - so.clear(element, toastString); - if (isShown) { - Fn(element, showClass); - } + bo.clear(element, toastString); + if (isShown) qn(element, showClass); super.dispose(); } } - __publicField(Toast, "selector", toastSelector); - __publicField(Toast, "init", toastInitCallback); - __publicField(Toast, "getInstance", getToastInstance); - const componentsList = { + const componentsList = /* @__PURE__ */ new Map(); + [ Alert, Button, Carousel, @@ -3275,7 +2804,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy Popover, Tab, Toast - }; + ].forEach((c) => componentsList.set(c.prototype.name, c)); const initComponentDataAPI = (callback, collection) => { [...collection].forEach((x2) => callback(x2)); }; @@ -3283,37 +2812,40 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const compData = L.getAllFor(component); if (compData) { [...compData].forEach(([element, instance]) => { - if (context.contains(element)) instance.dispose(); + if (context.contains(element)) { + instance.dispose(); + } }); } }; const initCallback = (context) => { const lookUp = context && context.nodeName ? context : document; - const elemCollection = [...be("*", lookUp)]; - $n(componentsList).forEach((cs) => { + const elemCollection = [...ke("*", lookUp)]; + componentsList.forEach((cs) => { const { init, selector } = cs; initComponentDataAPI( init, - elemCollection.filter((item) => xo(item, selector)) + elemCollection.filter((item) => ve(item, selector)) ); }); }; const removeDataAPI = (context) => { const lookUp = context && context.nodeName ? context : document; - Zn(componentsList).forEach((comp) => { - removeComponentDataAPI(comp, lookUp); + componentsList.forEach((comp) => { + removeComponentDataAPI(comp.prototype.name, lookUp); }); }; if (document.body) initCallback(); else { - E$1(document, "DOMContentLoaded", () => initCallback(), { once: true }); + E(document, "DOMContentLoaded", () => initCallback(), { + once: true + }); } exports.Alert = Alert; exports.Button = Button; exports.Carousel = Carousel; exports.Collapse = Collapse; exports.Dropdown = Dropdown; - exports.Listener = eventListener; exports.Modal = Modal; exports.Offcanvas = Offcanvas; exports.Popover = Popover; diff --git a/dist/bootstrap-native/bootstrap-native.min.js b/dist/bootstrap-native/bootstrap-native.min.js index 87a137fa6..2a1f64599 100644 --- a/dist/bootstrap-native/bootstrap-native.min.js +++ b/dist/bootstrap-native/bootstrap-native.min.js @@ -1,176 +1 @@ -var BSN=function(D){"use strict";var _a=Object.defineProperty;var Ba=(D,W,ut)=>W in D?_a(D,W,{enumerable:!0,configurable:!0,writable:!0,value:ut}):D[W]=ut;var d=(D,W,ut)=>Ba(D,typeof W!="symbol"?W+"":W,ut);const W={},ut=t=>{const{type:s,currentTarget:e}=t;[...W[s]].forEach(([n,o])=>{/* istanbul ignore else @preserve */e===n&&[...o].forEach(([i,a])=>{i.apply(n,[t]),typeof a=="object"&&a.once&&N(n,s,i,a)})})},I=(t,s,e,n)=>{/* istanbul ignore else @preserve */W[s]||(W[s]=new Map);const o=W[s];/* istanbul ignore else @preserve */o.has(t)||o.set(t,new Map);const i=o.get(t),{size:a}=i;i.set(e,n);/* istanbul ignore else @preserve */a||t.addEventListener(s,ut,n)},N=(t,s,e,n)=>{const o=W[s],i=o&&o.get(t),a=i&&i.get(e),c=a!==void 0?a:n;/* istanbul ignore else @preserve */i&&i.has(e)&&i.delete(e);/* istanbul ignore else @preserve */o&&(!i||!i.size)&&o.delete(t);/* istanbul ignore else @preserve */(!o||!o.size)&&delete W[s];/* istanbul ignore else @preserve */(!i||!i.size)&&t.removeEventListener(s,ut,c)},No=Object.freeze(Object.defineProperty({__proto__:null,addListener:I,globalListener:ut,off:N,on:I,registry:W,removeListener:N},Symbol.toStringTag,{value:"Module"})),Ms="aria-describedby",Ce="aria-expanded",ie="aria-hidden",Se="aria-modal",_s="aria-pressed",Ke="aria-selected",Oo="DOMContentLoaded",Ye="focus",Ue="focusin",Bs="focusout",Pe="keydown",Mo="keyup",O="click",Rs="mousedown",_o="hover",De="mouseenter",qe="mouseleave",Bo="pointerdown",Ro="pointermove",Fo="pointerup",He="resize",Ze="scroll",Je="touchstart",Wo="dragstart",jo='a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]',Ge="ArrowDown",Qe="ArrowUp",Fs="ArrowLeft",Ws="ArrowRight",ts="Escape",zo="transitionDuration",Vo="transitionDelay",es="transitionend",js="transitionProperty",Xo=navigator.userAgentData,xe=Xo,{userAgent:Ko}=navigator,Ae=Ko,zs=/iPhone|iPad|iPod|Android/i;// istanbul ignore else @preserve -xe?xe.brands.some(t=>zs.test(t.brand)):zs.test(Ae);const Vs=/(iPhone|iPod|iPad)/,Yo=xe?xe.brands.some(t=>Vs.test(t.brand)):Vs.test(Ae);Ae&&Ae.includes("Firefox");const{head:Le}=document;["webkitPerspective","perspective"].some(t=>t in Le.style);const Xs=(t,s,e,n)=>{const o=n||!1;t.addEventListener(s,e,o)},Ks=(t,s,e,n)=>{const o=n||!1;t.removeEventListener(s,e,o)},Uo=(t,s,e,n)=>{const o=i=>{/* istanbul ignore else @preserve */(i.target===t||i.currentTarget===t)&&(e.apply(t,[i]),Ks(t,s,o,n))};Xs(t,s,o,n)},ae=()=>{};(()=>{let t=!1;try{const s=Object.defineProperty({},"passive",{get:()=>(t=!0,t)});// istanbul ignore next @preserve -Uo(document,Oo,ae,s)}catch{}return t})(),["webkitTransform","transform"].some(t=>t in Le.style),["webkitAnimation","animation"].some(t=>t in Le.style),["webkitTransition","transition"].some(t=>t in Le.style);const nt=(t,s)=>t.getAttribute(s),ce=(t,s)=>t.hasAttribute(s),M=(t,s,e)=>t.setAttribute(s,e),Ht=(t,s)=>t.removeAttribute(s),h=(t,...s)=>{t.classList.add(...s)},v=(t,...s)=>{t.classList.remove(...s)},f=(t,s)=>t.classList.contains(s),le=t=>t!=null&&typeof t=="object"||!1,H=t=>le(t)&&typeof t.nodeType=="number"&&[1,2,3,4,5,6,7,8,9,10,11].some(s=>t.nodeType===s)||!1,y=t=>H(t)&&t.nodeType===1||!1,Wt=new Map,xt={data:Wt,set:(t,s,e)=>{if(!y(t))return;// istanbul ignore else @preserve -Wt.has(s)||Wt.set(s,new Map),Wt.get(s).set(t,e)},getAllFor:t=>Wt.get(t)||null,get:(t,s)=>{if(!y(t)||!s)return null;const e=xt.getAllFor(s);return t&&e&&e.get(t)||null},remove:(t,s)=>{const e=xt.getAllFor(s);if(!e||!y(t))return;e.delete(t);// istanbul ignore else @preserve -e.size===0&&Wt.delete(s)}},z=(t,s)=>xt.get(t,s),re=t=>typeof t=="string"||!1,qo=t=>le(t)&&t.constructor.name==="Window"||!1,Ys=t=>H(t)&&t.nodeType===9||!1,$=t=>qo(t)?t.document:Ys(t)?t:H(t)?t.ownerDocument:window.document,lt=(t,...s)=>Object.assign(t,...s),mt=t=>{if(!t)return;if(re(t))return $().createElement(t);const{tagName:s}=t,e=mt(s);if(!e)return;const n={...t};return delete n.tagName,lt(e,n)},b=(t,s)=>t.dispatchEvent(s),V=(t,s)=>{const e=getComputedStyle(t),n=s.replace("webkit","Webkit").replace(/([A-Z])/g,"-$1").toLowerCase();return e.getPropertyValue(n)},Zo=t=>{const s=V(t,js),e=V(t,Vo),n=e.includes("ms")?1:1e3,o=s&&s!=="none"?parseFloat(e)*n:0;return Number.isNaN(o)?0:o},de=t=>{const s=V(t,js),e=V(t,zo),n=e.includes("ms")?1:1e3,o=s&&s!=="none"?parseFloat(e)*n:0;return Number.isNaN(o)?0:o},x=(t,s)=>{let e=0;const n=new Event(es),o=de(t),i=Zo(t);if(o){const a=c=>{// istanbul ignore else @preserve -c.target===t&&(s.apply(t,[c]),t.removeEventListener(es,a),e=1)};t.addEventListener(es,a),setTimeout(()=>{// istanbul ignore next @preserve -e||b(t,n)},o+i+17)}else s.apply(t,[n])},rt=(t,s)=>t.focus(s),Us=t=>["true",!0].includes(t)?!0:["false",!1].includes(t)?!1:["null","",null,void 0].includes(t)?null:t!==""&&!Number.isNaN(+t)?+t:t,ke=t=>Object.entries(t),jt=t=>t.toLowerCase(),Jo=(t,s,e,n)=>{const o={...e},i={...t.dataset},a={...s},c={},r="title";return ke(i).forEach(([l,u])=>{const m=typeof l=="string"&&l.includes(n)?l.replace(n,"").replace(/[A-Z]/g,w=>jt(w)):l;c[m]=Us(u)}),ke(o).forEach(([l,u])=>{o[l]=Us(u)}),ke(s).forEach(([l,u])=>{// istanbul ignore else @preserve -l in o?a[l]=o[l]:l in c?a[l]=c[l]:a[l]=l===r?nt(t,r):u}),a},ss=t=>Object.keys(t),Go=t=>Object.values(t),E=(t,s)=>{const e=new CustomEvent(t,{cancelable:!0,bubbles:!0});// istanbul ignore else @preserve -return le(s)&<(e,s),e},ot={passive:!0},At=t=>t.offsetHeight,A=(t,s)=>{ke(s).forEach(([e,n])=>{if(n&&re(e)&&e.includes("--"))t.style.setProperty(e,n);else{const o={};o[e]=n,lt(t.style,o)}})},ns=t=>le(t)&&t.constructor.name==="Map"||!1,Qo=t=>typeof t=="number"||!1,vt=new Map,g={set:(t,s,e,n)=>{if(!y(t))return;// istanbul ignore else @preserve -if(n&&n.length){// istanbul ignore else @preserve -vt.has(t)||vt.set(t,new Map),vt.get(t).set(n,setTimeout(s,e))}else vt.set(t,setTimeout(s,e))},get:(t,s)=>{if(!y(t))return null;const e=vt.get(t);return s&&e&&ns(e)?e.get(s)||null:Qo(e)?e:null},clear:(t,s)=>{if(!y(t))return;const e=vt.get(t);if(s&&s.length&&ns(e)){clearTimeout(e.get(s)),e.delete(s);// istanbul ignore else @preserve -e.size===0&&vt.delete(t)}else clearTimeout(e),vt.delete(t)}},J=(t,s)=>(H(s)?s:$()).querySelectorAll(t),os=new Map;function ti(t){const{shiftKey:s,code:e}=t,n=$(this),o=[...J(jo,this)].filter(c=>!ce(c,"disabled")&&!nt(c,ie));if(!o.length)return;const i=o[0],a=o[o.length-1];// istanbul ignore else @preserve -e==="Tab"&&(s&&n.activeElement===i?(a.focus(),t.preventDefault()):!s&&n.activeElement===a&&(i.focus(),t.preventDefault()))}const ei=t=>os.has(t)===!0,Ie=t=>{const s=ei(t);(s?Ks:Xs)(t,"keydown",ti),s?os.delete(t):os.set(t,!0)},is=(t,s)=>{const{width:e,height:n,top:o,right:i,bottom:a,left:c}=t.getBoundingClientRect();let r=1,l=1;if(s&&y(t)){const{offsetWidth:u,offsetHeight:m}=t;r=u>0?Math.round(e)/u:1,l=m>0?Math.round(n)/m:1}return{width:e/r,height:n/l,top:o/l,right:i/r,bottom:a/l,left:c/r,x:c/r,y:o/l}},Lt=t=>$(t).body,bt=t=>$(t).documentElement,qs=t=>H(t)&&t.constructor.name==="ShadowRoot"||!1,si=t=>t.nodeName==="HTML"?t:y(t)&&t.assignedSlot||H(t)&&t.parentNode||qs(t)&&t.host||bt(t);let Zs=0,Js=0;const zt=new Map,Gs=(t,s)=>{let e=s?Zs:Js;if(s){const n=Gs(t),o=zt.get(n)||new Map;zt.has(n)||zt.set(n,o),ns(o)&&!o.has(s)?(o.set(s,e),Zs+=1):e=o.get(s)}else{const n=t.id||t;zt.has(n)?e=zt.get(n):(zt.set(n,e),Js+=1)}return e},he=t=>{var s;return t?Ys(t)?t.defaultView:H(t)?(s=t==null?void 0:t.ownerDocument)==null?void 0:s.defaultView:t:window},ni=t=>Array.isArray(t)||!1,Qs=t=>{if(!H(t))return!1;const{top:s,bottom:e}=is(t),{clientHeight:n}=bt(t);return s<=n&&e>=0},oi=t=>typeof t=="function"||!1,ii=t=>le(t)&&t.constructor.name==="NodeList"||!1,wt=t=>bt(t).dir==="rtl",ai=t=>H(t)&&["TABLE","TD","TH"].includes(t.nodeName)||!1,_=(t,s)=>t?t.closest(s)||_(t.getRootNode().host,s):null,L=(t,s)=>y(t)?t:(H(s)?s:$()).querySelector(t),ci=(t,s)=>(H(s)?s:$()).getElementsByTagName(t),dt=(t,s)=>(s&&H(s)?s:$()).getElementsByClassName(t),tn=(t,s)=>t.matches(s),F="fade",p="show",Ne="data-bs-dismiss",Oe="alert",en="Alert",li="5.0.15";class it{constructor(s,e){d(this,"_toggleEventListeners",()=>{});let n;try{if(y(s))n=s;else if(re(s)){n=L(s);// istanbul ignore else @preserve -if(!n)throw Error(`"${s}" is not a valid selector.`)}else throw Error("your target is not an instance of HTMLElement.")}catch(i){throw Error(`${this.name} Error: ${i.message}`)}const o=xt.get(n,this.name);// istanbul ignore else @preserve -o&&o._toggleEventListeners(),this.element=n,this.options=this.defaults&&ss(this.defaults).length?Jo(n,this.defaults,e||{},"bs"):{},xt.set(n,this.name,this)}get version(){return li}get name(){return"BaseComponent"}get defaults(){return{}}dispose(){xt.remove(this.element,this.name),ss(this).forEach(s=>{delete this[s]})}}const ri=`.${Oe}`,di=`[${Ne}="${Oe}"]`,hi=t=>z(t,en),fi=t=>new Vt(t),sn=E(`close.bs.${Oe}`),gi=E(`closed.bs.${Oe}`),nn=t=>{const{element:s}=t;b(s,gi),t._toggleEventListeners(),t.dispose(),s.remove()};class Vt extends it{constructor(e){super(e);d(this,"dismiss");d(this,"close",()=>{const{element:e}=this;// istanbul ignore else @preserve -e&&f(e,p)&&(b(e,sn),sn.defaultPrevented||(v(e,p),f(e,F)?x(e,()=>nn(this)):nn(this)))});d(this,"_toggleEventListeners",e=>{const n=e?I:N,{dismiss:o,close:i}=this;// istanbul ignore else @preserve -o&&n(o,O,i)});this.dismiss=L(di,this.element),this._toggleEventListeners(!0)}get name(){return en}dispose(){this._toggleEventListeners(),super.dispose()}}d(Vt,"selector",ri),d(Vt,"init",fi),d(Vt,"getInstance",hi);const C="active",at="data-bs-toggle",pi="button",on="Button",ui=`[${at}="${pi}"]`,mi=t=>z(t,on),vi=t=>new Xt(t);class Xt extends it{constructor(e){super(e);d(this,"isActive",!1);d(this,"toggle",e=>{e&&e.preventDefault();const{element:n,isActive:o}=this;!f(n,"disabled")&&!nt(n,"disabled")&&((o?v:h)(n,C),M(n,_s,o?"false":"true"),this.isActive=f(n,C))});d(this,"_toggleEventListeners",e=>{(e?I:N)(this.element,O,this.toggle)});const{element:n}=this;this.isActive=f(n,C),M(n,_s,String(!!this.isActive)),this._toggleEventListeners(!0)}get name(){return on}dispose(){this._toggleEventListeners(),super.dispose()}}d(Xt,"selector",ui),d(Xt,"init",vi),d(Xt,"getInstance",mi);const as="data-bs-target",kt="carousel",an="Carousel",cn="data-bs-parent",bi="data-bs-container",X=t=>{const s=[as,cn,bi,"href"],e=$(t);return s.map(n=>{const o=nt(t,n);return o?n===cn?_(t,o):L(o,e):null}).filter(n=>n)[0]},fe=`[data-bs-ride="${kt}"]`,G=`${kt}-item`,cs="data-bs-slide-to",$t="data-bs-slide",Et="paused",ln={pause:"hover",keyboard:!1,touch:!0,interval:5e3},ht=t=>z(t,an),wi=t=>new Kt(t);let ge=0,Me=0,ls=0;const rs=E(`slide.bs.${kt}`),ds=E(`slid.bs.${kt}`),rn=t=>{const{index:s,direction:e,element:n,slides:o,options:i}=t;// istanbul ignore else @preserve -if(t.isAnimating){const a=fs(t),c=e==="left"?"next":"prev",r=e==="left"?"start":"end";h(o[s],C),v(o[s],`${G}-${c}`),v(o[s],`${G}-${r}`),v(o[a],C),v(o[a],`${G}-${r}`),b(n,ds),g.clear(n,$t),t.cycle&&!$(n).hidden&&i.interval&&!t.isPaused&&t.cycle()}};function $i(){const t=ht(this);// istanbul ignore else @preserve -t&&!t.isPaused&&!g.get(this,Et)&&h(this,Et)}function Ei(){const t=ht(this);// istanbul ignore else @preserve -t&&t.isPaused&&!g.get(this,Et)&&t.cycle()}function Ti(t){t.preventDefault();const s=_(this,fe)||X(this),e=ht(s);// istanbul ignore else @preserve -if(e&&!e.isAnimating){const n=+(nt(this,cs)||0);// istanbul ignore else @preserve -this&&!f(this,C)&&!Number.isNaN(n)&&e.to(n)}}function yi(t){t.preventDefault();const s=_(this,fe)||X(this),e=ht(s);// istanbul ignore else @preserve -if(e&&!e.isAnimating){const n=nt(this,$t);// istanbul ignore else @preserve -n==="next"?e.next():n==="prev"&&e.prev()}}const Ci=({code:t,target:s})=>{const e=$(s),[n]=[...J(fe,e)].filter(i=>Qs(i)),o=ht(n);// istanbul ignore next @preserve -if(o&&!o.isAnimating&&!/textarea|input/i.test(s.nodeName)){const i=wt(n),a=i?Fs:Ws,c=i?Ws:Fs;// istanbul ignore else @preserve -t===c?o.prev():t===a&&o.next()}};function dn(t){const{target:s}=t,e=ht(this);// istanbul ignore next @preserve -e&&e.isTouch&&(e.indicator&&!e.indicator.contains(s)||!e.controls.includes(s))&&(t.stopImmediatePropagation(),t.stopPropagation(),t.preventDefault())}function Si(t){const{target:s}=t,e=ht(this);// istanbul ignore else @preserve -if(e&&!e.isAnimating&&!e.isTouch){const{controls:n,indicators:o}=e;// istanbul ignore else @preserve -if(![...n,...o].every(i=>i===s||i.contains(s))){ge=t.pageX;// istanbul ignore else @preserve -this.contains(s)&&(e.isTouch=!0,hn(e,!0))}}}const Pi=t=>{Me=t.pageX},Di=t=>{var o;const{target:s}=t,e=$(s),n=[...J(fe,e)].map(i=>ht(i)).find(i=>i.isTouch);// istanbul ignore else @preserve -if(n){const{element:i,index:a}=n,c=wt(i);if(ls=t.pageX,n.isTouch=!1,hn(n),!((o=e.getSelection())!=null&&o.toString().length)&&i.contains(s)&&Math.abs(ge-ls)>120){// istanbul ignore else @preserve -Mege&&n.to(a+(c?1:-1))}ge=0,Me=0,ls=0}},hs=(t,s)=>{const{indicators:e}=t;[...e].forEach(n=>v(n,C));// istanbul ignore else @preserve -t.indicators[s]&&h(e[s],C)},hn=(t,s)=>{const{element:e}=t,n=s?I:N;n($(e),Ro,Pi,ot),n($(e),Fo,Di,ot)},fs=t=>{const{slides:s,element:e}=t,n=L(`.${G}.${C}`,e);return y(n)?[...s].indexOf(n):-1};class Kt extends it{constructor(e,n){super(e,n);d(this,"_toggleEventListeners",e=>{const{element:n,options:o,slides:i,controls:a,indicators:c}=this,{touch:r,pause:l,interval:u,keyboard:m}=o,w=e?I:N;l&&u&&(w(n,De,$i),w(n,qe,Ei)),r&&i.length>2&&(w(n,Bo,Si,ot),w(n,Je,dn,{passive:!1}),w(n,Wo,dn,{passive:!1}));// istanbul ignore else @preserve -a.length&&a.forEach(B=>{// istanbul ignore else @preserve -B&&w(B,O,yi)});// istanbul ignore else @preserve -c.length&&c.forEach(B=>{w(B,O,Ti)}),m&&w($(n),Pe,Ci)});const{element:o}=this;this.direction=wt(o)?"right":"left",this.isTouch=!1,this.slides=dt(G,o);const{slides:i}=this;if(i.length>=2){const a=fs(this),c=[...i].find(u=>tn(u,`.${G}-next,.${G}-next`));this.index=a;const r=$(o);this.controls=[...J(`[${$t}]`,o),...J(`[${$t}][${as}="#${o.id}"]`,r)].filter((u,m,w)=>m===w.indexOf(u)),this.indicator=L(`.${kt}-indicators`,o),this.indicators=[...this.indicator?J(`[${cs}]`,this.indicator):[],...J(`[${cs}][${as}="#${o.id}"]`,r)].filter((u,m,w)=>m===w.indexOf(u));const{options:l}=this;this.options.interval=l.interval===!0?ln.interval:l.interval;// istanbul ignore next @preserve - impossible to test -c?this.index=[...i].indexOf(c):a<0&&(this.index=0,h(i[0],C),this.indicators.length&&hs(this,0));// istanbul ignore else @preserve -this.indicators.length&&hs(this,this.index),this._toggleEventListeners(!0),l.interval&&this.cycle()}}get name(){return an}get defaults(){return ln}get isPaused(){return f(this.element,Et)}get isAnimating(){return L(`.${G}-next,.${G}-prev`,this.element)!==null}cycle(){const{element:e,options:n,isPaused:o,index:i}=this;g.clear(e,kt),o&&(g.clear(e,Et),v(e,Et)),g.set(e,()=>{// istanbul ignore else @preserve -this.element&&!this.isPaused&&!this.isTouch&&Qs(e)&&this.to(i+1)},n.interval,kt)}pause(){const{element:e,options:n}=this;// istanbul ignore else @preserve -!this.isPaused&&n.interval&&(h(e,Et),g.set(e,()=>{},1,Et))}next(){// istanbul ignore else @preserve -this.isAnimating||this.to(this.index+1)}prev(){// istanbul ignore else @preserve -this.isAnimating||this.to(this.index-1)}to(e){const{element:n,slides:o,options:i}=this,a=fs(this),c=wt(n);let r=e;if(!this.isAnimating&&a!==r&&!g.get(n,$t)){// istanbul ignore else @preserve -ar||a===o.length-1&&r===0)&&(this.direction=c?"left":"right");const{direction:l}=this;r<0?r=o.length-1:r>=o.length&&(r=0);const u=l==="left"?"next":"prev",m=l==="left"?"start":"end",w={relatedTarget:o[r],from:a,to:r,direction:l};lt(rs,w),lt(ds,w),b(n,rs),rs.defaultPrevented||(this.index=r,hs(this,r),de(o[r])&&f(n,"slide")?g.set(n,()=>{h(o[r],`${G}-${u}`),At(o[r]),h(o[r],`${G}-${m}`),h(o[a],`${G}-${m}`),x(o[r],()=>this.slides&&this.slides.length&&rn(this))},0,$t):(h(o[r],C),v(o[a],C),g.set(n,()=>{g.clear(n,$t);// istanbul ignore else @preserve -n&&i.interval&&!this.isPaused&&this.cycle(),b(n,ds)},0,$t)))}}dispose(){const{isAnimating:e}=this,n={...this,isAnimating:e};this._toggleEventListeners(),super.dispose();// istanbul ignore next @preserve - impossible to test -n.isAnimating&&x(n.slides[n.index],()=>{rn(n)})}}d(Kt,"selector",fe),d(Kt,"init",wi),d(Kt,"getInstance",ht);const It="collapsing",K="collapse",fn="Collapse",Hi=`.${K}`,gn=`[${at}="${K}"]`,xi={parent:null},_e=t=>z(t,fn),Ai=t=>new Yt(t),pn=E(`show.bs.${K}`),Li=E(`shown.bs.${K}`),un=E(`hide.bs.${K}`),ki=E(`hidden.bs.${K}`),Ii=t=>{const{element:s,parent:e,triggers:n}=t;b(s,pn),pn.defaultPrevented||(g.set(s,ae,17),e&&g.set(e,ae,17),h(s,It),v(s,K),A(s,{height:`${s.scrollHeight}px`}),x(s,()=>{g.clear(s),e&&g.clear(e),n.forEach(o=>M(o,Ce,"true")),v(s,It),h(s,K),h(s,p),A(s,{height:""}),b(s,Li)}))},mn=t=>{const{element:s,parent:e,triggers:n}=t;b(s,un),un.defaultPrevented||(g.set(s,ae,17),e&&g.set(e,ae,17),A(s,{height:`${s.scrollHeight}px`}),v(s,K),v(s,p),h(s,It),At(s),A(s,{height:"0px"}),x(s,()=>{g.clear(s);// istanbul ignore else @preserve -e&&g.clear(e),n.forEach(o=>M(o,Ce,"false")),v(s,It),h(s,K),A(s,{height:""}),b(s,ki)}))},Ni=t=>{const{target:s}=t,e=s&&_(s,gn),n=e&&X(e),o=n&&_e(n);// istanbul ignore else @preserve -o&&o.toggle(),e&&e.tagName==="A"&&t.preventDefault()};class Yt extends it{constructor(e,n){super(e,n);d(this,"_toggleEventListeners",e=>{const n=e?I:N,{triggers:o}=this;// istanbul ignore else @preserve -o.length&&o.forEach(i=>n(i,O,Ni))});const{element:o,options:i}=this,a=$(o);this.triggers=[...J(gn,a)].filter(c=>X(c)===o),this.parent=y(i.parent)?i.parent:re(i.parent)?X(o)||L(i.parent,a):null,this._toggleEventListeners(!0)}get name(){return fn}get defaults(){return xi}hide(){const{triggers:e,element:n}=this;// istanbul ignore else @preserve -if(!g.get(n)){mn(this);// istanbul ignore else @preserve -e.length&&e.forEach(o=>h(o,`${K}d`))}}show(){const{element:e,parent:n,triggers:o}=this;let i,a;if(n&&(i=[...J(`.${K}.${p}`,n)].find(c=>_e(c)),a=i&&_e(i)),(!n||!g.get(n))&&!g.get(e)){a&&i!==e&&(mn(a),a.triggers.forEach(c=>{h(c,`${K}d`)})),Ii(this);// istanbul ignore else @preserve -o.length&&o.forEach(c=>v(c,`${K}d`))}}toggle(){f(this.element,p)?this.hide():this.show()}dispose(){this._toggleEventListeners(),super.dispose()}}d(Yt,"selector",Hi),d(Yt,"init",Ai),d(Yt,"getInstance",_e);const Nt=["dropdown","dropup","dropstart","dropend"],vn="Dropdown",bn="dropdown-menu",wn=t=>{const s=_(t,"A");return t.tagName==="A"&&ce(t,"href")&&nt(t,"href").slice(-1)==="#"||s&&ce(s,"href")&&nt(s,"href").slice(-1)==="#"},[et,gs,ps,us]=Nt,$n=`[${at}="${et}"]`,Ut=t=>z(t,vn),Oi=t=>new qt(t),Mi=`${bn}-end`,En=[et,gs],Tn=[ps,us],yn=["A","BUTTON"],_i={offset:5,display:"dynamic"},ms=E(`show.bs.${et}`),Cn=E(`shown.bs.${et}`),vs=E(`hide.bs.${et}`),Sn=E(`hidden.bs.${et}`),Pn=E(`updated.bs.${et}`),Dn=t=>{const{element:s,menu:e,parentElement:n,options:o}=t,{offset:i}=o;// istanbul ignore else @preserve: this test requires a navbar -if(V(e,"position")!=="static"){const a=wt(s),c=f(e,Mi);["margin","top","bottom","left","right"].forEach(R=>{const St={};St[R]="",A(e,St)});let l=Nt.find(R=>f(n,R))||et;const u={dropdown:[i,0,0],dropup:[0,0,i],dropstart:a?[-1,0,0,i]:[-1,i,0],dropend:a?[-1,i,0]:[-1,0,0,i]},m={dropdown:{top:"100%"},dropup:{top:"auto",bottom:"100%"},dropstart:a?{left:"100%",right:"auto"}:{left:"auto",right:"100%"},dropend:a?{left:"auto",right:"100%"}:{left:"100%",right:"auto"},menuStart:a?{right:"0",left:"auto"}:{right:"auto",left:"0"},menuEnd:a?{right:"auto",left:"0"}:{right:"0",left:"auto"}},{offsetWidth:w,offsetHeight:B}=e,{clientWidth:gt,clientHeight:T}=bt(s),{left:Y,top:Z,width:se,height:pt}=is(s),S=Y-w-i<0,st=Y+w+se+i>=gt,ct=Z+B+i>=T,j=Z+B+pt+i>=T,U=Z-B-i<0,P=(!a&&c||a&&!c)&&Y+se-w<0,ne=(a&&c||!a&&!c)&&Y+w>=gt;if(Tn.includes(l)&&S&&st&&(l=et),l===ps&&(a?st:S)&&(l=us),l===us&&(a?S:st)&&(l=ps),l===gs&&U&&!j&&(l=et),l===et&&j&&!U&&(l=gs),Tn.includes(l)&&ct&<(m[l],{top:"auto",bottom:0}),En.includes(l)&&(P||ne)){let R={left:"auto",right:"auto"};// istanbul ignore else @preserve -!P&&ne&&!a&&(R={left:"auto",right:0});// istanbul ignore else @preserve -P&&!ne&&a&&(R={left:0,right:"auto"});// istanbul ignore else @preserve -R&<(m[l],R)}const Ct=u[l];if(A(e,{...m[l],margin:`${Ct.map(R=>R&&`${R}px`).join(" ")}`}),En.includes(l)&&c){// istanbul ignore else @preserve -c&&A(e,m[!a&&P||a&&ne?"menuStart":"menuEnd"])}b(n,Pn)}},Bi=t=>[...t.children].map(s=>{if(s&&yn.includes(s.tagName))return s;const{firstElementChild:e}=s;return e&&yn.includes(e.tagName)?e:null}).filter(s=>s),Hn=t=>{const{element:s,options:e}=t,n=t.open?I:N,o=$(s);n(o,O,xn),n(o,Ye,xn),n(o,Pe,Fi),n(o,Mo,Wi);// istanbul ignore else @preserve -e.display==="dynamic"&&[Ze,He].forEach(i=>{n(he(s),i,ji,ot)})},Be=t=>{const s=[...Nt,"btn-group","input-group"].map(e=>dt(`${e} ${p}`,$(t))).find(e=>e.length);if(s&&s.length)return[...s[0].children].find(e=>Nt.some(n=>n===nt(e,at)))},xn=t=>{const{target:s,type:e}=t;// istanbul ignore else @preserve -if(s&&y(s)){const n=Be(s),o=n&&Ut(n);// istanbul ignore else @preserve -if(o){const{parentElement:i,menu:a}=o,c=i&&i.contains(s)&&(s.tagName==="form"||_(s,"form")!==null);[O,Rs].includes(e)&&wn(s)&&t.preventDefault();// istanbul ignore else @preserve -!c&&e!==Ye&&s!==n&&s!==a&&o.hide()}}},Ri=t=>{const{target:s}=t,e=s&&_(s,$n),n=e&&Ut(e);// istanbul ignore else @preserve -if(n){t.stopPropagation(),n.toggle();// istanbul ignore else @preserve -e&&wn(e)&&t.preventDefault()}},Fi=t=>{// istanbul ignore else @preserve -[Ge,Qe].includes(t.code)&&t.preventDefault()};function Wi(t){const{code:s}=t,e=Be(this),n=e&&Ut(e),{activeElement:o}=e&&$(e);// istanbul ignore else @preserve -if(n&&o){const{menu:i,open:a}=n,c=Bi(i);if(c&&c.length&&[Ge,Qe].includes(s)){let r=c.indexOf(o);// istanbul ignore else @preserve -o===e?r=0:s===Qe?r=r>1?r-1:0:s===Ge&&(r=r{(e?I:N)(this.element,O,Ri)});const{parentElement:o}=this.element,[i]=dt(bn,o);i&&(this.parentElement=o,this.menu=i,this._toggleEventListeners(!0))}get name(){return vn}get defaults(){return _i}toggle(){this.open?this.hide():this.show()}show(){const{element:e,open:n,menu:o,parentElement:i}=this;// istanbul ignore else @preserve -if(!n){const a=Be(e),c=a&&Ut(a);c&&c.hide(),[ms,Cn,Pn].forEach(r=>{r.relatedTarget=e}),b(i,ms),ms.defaultPrevented||(h(o,p),h(i,p),M(e,Ce,"true"),Dn(this),this.open=!n,rt(e),Hn(this),b(i,Cn))}}hide(){const{element:e,open:n,menu:o,parentElement:i}=this;// istanbul ignore else @preserve -n&&([vs,Sn].forEach(a=>{a.relatedTarget=e}),b(i,vs),vs.defaultPrevented||(v(o,p),v(i,p),M(e,Ce,"false"),this.open=!n,Hn(this),b(i,Sn)))}dispose(){this.open&&this.hide(),this._toggleEventListeners(),super.dispose()}}d(qt,"selector",$n),d(qt,"init",Oi),d(qt,"getInstance",Ut);const q="modal",bs="Modal",ws="Offcanvas",zi="fixed-top",Vi="fixed-bottom",An="sticky-top",Ln="position-sticky",kn=t=>[...dt(zi,t),...dt(Vi,t),...dt(An,t),...dt(Ln,t),...dt("is-fixed",t)],Xi=t=>{const s=Lt(t);A(s,{paddingRight:"",overflow:""});const e=kn(s);// istanbul ignore else @preserve -e.length&&e.forEach(n=>{A(n,{paddingRight:"",marginRight:""})})},In=t=>{const{clientWidth:s}=bt(t),{innerWidth:e}=he(t);return Math.abs(e-s)},Nn=(t,s)=>{const e=Lt(t),n=parseInt(V(e,"paddingRight"),10),i=V(e,"overflow")==="hidden"&&n?0:In(t),a=kn(e);// istanbul ignore else @preserve -if(s){A(e,{overflow:"hidden",paddingRight:`${n+i}px`});// istanbul ignore else @preserve -a.length&&a.forEach(c=>{const r=V(c,"paddingRight");c.style.paddingRight=`${parseInt(r,10)+i}px`;// istanbul ignore else @preserve -if([An,Ln].some(l=>f(c,l))){const l=V(c,"marginRight");c.style.marginRight=`${parseInt(l,10)-i}px`}})}},Q="offcanvas",Tt=mt({tagName:"div",className:"popup-container"}),On=(t,s)=>{const e=H(s)&&s.nodeName==="BODY",n=H(s)&&!e?s:Tt,o=e?s:Lt(t);// istanbul ignore else @preserve -H(t)&&(n===Tt&&o.append(Tt),n.append(t))},Mn=(t,s)=>{const e=H(s)&&s.nodeName==="BODY",n=H(s)&&!e?s:Tt;// istanbul ignore else @preserve -H(t)&&(t.remove(),n===Tt&&!Tt.children.length&&Tt.remove())},$s=(t,s)=>{const e=H(s)&&s.nodeName!=="BODY"?s:Tt;return H(t)&&e.contains(t)},_n="backdrop",Bn=`${q}-${_n}`,Rn=`${Q}-${_n}`,Fn=`.${q}.${p}`,Es=`.${Q}.${p}`,k=mt("div"),Ot=t=>L(`${Fn},${Es}`,$(t)),Ts=t=>{const s=t?Bn:Rn;[Bn,Rn].forEach(e=>{v(k,e)}),h(k,s)},Wn=(t,s,e)=>{Ts(e),On(k,Lt(t)),s&&h(k,F)},jn=()=>{f(k,p)||(h(k,p),At(k))},Re=()=>{v(k,p)},zn=t=>{Ot(t)||(v(k,F),Mn(k,Lt(t)),Xi(t))},Vn=t=>y(t)&&V(t,"visibility")!=="hidden"&&t.offsetParent!==null,Ki=`.${q}`,Xn=`[${at}="${q}"]`,Yi=`[${Ne}="${q}"]`,Kn=`${q}-static`,Ui={backdrop:!0,keyboard:!0},pe=t=>z(t,bs),qi=t=>new Zt(t),Fe=E(`show.bs.${q}`),Yn=E(`shown.bs.${q}`),ys=E(`hide.bs.${q}`),Un=E(`hidden.bs.${q}`),qn=t=>{const{element:s}=t,e=In(s),{clientHeight:n,scrollHeight:o}=bt(s),{clientHeight:i,scrollHeight:a}=s,c=i!==a;// istanbul ignore next @preserve: impossible to test? -if(!c&&e){const l={[wt(s)?"paddingLeft":"paddingRight"]:`${e}px`};A(s,l)}Nn(s,c||n!==o)},Zn=(t,s)=>{const e=s?I:N,{element:n,update:o}=t;e(n,O,Gi),e(he(n),He,o,ot),e($(n),Pe,Ji)},Jn=t=>{const{triggers:s,element:e,relatedTarget:n}=t;zn(e),A(e,{paddingRight:"",display:""}),Zn(t);const o=Fe.relatedTarget||s.find(Vn);// istanbul ignore else @preserve -o&&rt(o),Un.relatedTarget=n,b(e,Un),Ie(e)},Gn=t=>{const{element:s,relatedTarget:e}=t;rt(s),Zn(t,!0),Yn.relatedTarget=e,b(s,Yn),Ie(s)},Qn=t=>{const{element:s,hasFade:e}=t;A(s,{display:"block"}),qn(t);// istanbul ignore else @preserve -Ot(s)||A(Lt(s),{overflow:"hidden"}),h(s,p),Ht(s,ie),M(s,Se,"true"),e?x(s,()=>Gn(t)):Gn(t)},to=t=>{const{element:s,options:e,hasFade:n}=t;e.backdrop&&n&&f(k,p)&&!Ot(s)?(Re(),x(k,()=>Jn(t))):Jn(t)},Zi=t=>{const{target:s}=t,e=s&&_(s,Xn),n=e&&X(e),o=n&&pe(n);// istanbul ignore else @preserve -if(o){// istanbul ignore else @preserve -e&&e.tagName==="A"&&t.preventDefault(),o.relatedTarget=e,o.toggle()}},Ji=({code:t,target:s})=>{const e=L(Fn,$(s)),n=e&&pe(e);// istanbul ignore else @preserve -if(n){const{options:o}=n;// istanbul ignore else @preserve -o.keyboard&&t===ts&&f(e,p)&&(n.relatedTarget=null,n.hide())}},Gi=t=>{var n,o;const{currentTarget:s}=t,e=s&&pe(s);// istanbul ignore else @preserve -if(e&&s&&!g.get(s)){const{options:i,isStatic:a,modalDialog:c}=e,{backdrop:r}=i,{target:l}=t,u=(o=(n=$(s))==null?void 0:n.getSelection())==null?void 0:o.toString().length,m=c.contains(l),w=l&&_(l,Yi);// istanbul ignore else @preserve -a&&!m?g.set(s,()=>{h(s,Kn),x(c,()=>Qi(e))},17):(w||!u&&!a&&!m&&r)&&(e.relatedTarget=w||null,e.hide(),t.preventDefault())}},Qi=t=>{const{element:s,modalDialog:e}=t,n=(de(e)||0)+17;v(s,Kn),g.set(s,()=>g.clear(s),n)};class Zt extends it{constructor(e,n){super(e,n);d(this,"update",()=>{// istanbul ignore else @preserve -f(this.element,p)&&qn(this)});d(this,"_toggleEventListeners",e=>{const n=e?I:N,{triggers:o}=this;// istanbul ignore else @preserve -o.length&&o.forEach(i=>n(i,O,Zi))});const{element:o}=this,i=L(`.${q}-dialog`,o);// istanbul ignore else @preserve -i&&(this.modalDialog=i,this.triggers=[...J(Xn,$(o))].filter(a=>X(a)===o),this.isStatic=this.options.backdrop==="static",this.hasFade=f(o,F),this.relatedTarget=null,this._toggleEventListeners(!0))}get name(){return bs}get defaults(){return Ui}toggle(){f(this.element,p)?this.hide():this.show()}show(){const{element:e,options:n,hasFade:o,relatedTarget:i}=this,{backdrop:a}=n;let c=0;// istanbul ignore else @preserve -if(!f(e,p)&&(Fe.relatedTarget=i||void 0,b(e,Fe),!Fe.defaultPrevented)){const r=Ot(e);// istanbul ignore else @preserve -if(r&&r!==e){const l=pe(r)||z(r,ws);// istanbul ignore else @preserve -l&&l.hide()}if(a)$s(k)?Ts(!0):Wn(e,o,!0),c=de(k),jn(),setTimeout(()=>Qn(this),c);else{Qn(this);// istanbul ignore else @preserve -r&&f(k,p)&&Re()}}}hide(){const{element:e,hasFade:n,relatedTarget:o}=this;// istanbul ignore else @preserve -if(f(e,p)){ys.relatedTarget=o||void 0,b(e,ys);// istanbul ignore else @preserve -ys.defaultPrevented||(v(e,p),M(e,ie,"true"),Ht(e,Se),n?x(e,()=>to(this)):to(this))}}dispose(){const e={...this},{modalDialog:n,hasFade:o}=e,i=()=>setTimeout(()=>super.dispose(),17);this.hide(),this._toggleEventListeners(),o?x(n,i):i()}}d(Zt,"selector",Ki),d(Zt,"init",qi),d(Zt,"getInstance",pe);const ta=`.${Q}`,Cs=`[${at}="${Q}"]`,ea=`[${Ne}="${Q}"]`,We=`${Q}-toggling`,sa={backdrop:!0,keyboard:!0,scroll:!1},ue=t=>z(t,ws),na=t=>new Jt(t),je=E(`show.bs.${Q}`),eo=E(`shown.bs.${Q}`),Ss=E(`hide.bs.${Q}`),so=E(`hidden.bs.${Q}`),oa=t=>{const{element:s}=t,{clientHeight:e,scrollHeight:n}=bt(s);Nn(s,e!==n)},no=(t,s)=>{const e=s?I:N,n=$(t.element);e(n,Pe,la),e(n,O,ca)},oo=t=>{const{element:s,options:e}=t;// istanbul ignore else @preserve -e.scroll||(oa(t),A(Lt(s),{overflow:"hidden"})),h(s,We),h(s,p),A(s,{visibility:"visible"}),x(s,()=>ra(t))},ia=t=>{const{element:s,options:e}=t,n=Ot(s);s.blur(),!n&&e.backdrop&&f(k,p)&&Re(),x(s,()=>da(t))},aa=t=>{const s=_(t.target,Cs),e=s&&X(s),n=e&&ue(e);// istanbul ignore else @preserve -if(n){n.relatedTarget=s,n.toggle();// istanbul ignore else @preserve -s&&s.tagName==="A"&&t.preventDefault()}},ca=t=>{const{target:s}=t,e=L(Es,$(s)),n=L(ea,e),o=e&&ue(e);// istanbul ignore else @preserve -if(o){const{options:i,triggers:a}=o,{backdrop:c}=i,r=_(s,Cs),l=$(e).getSelection();// istanbul ignore else: a filter is required here @preserve -if(!k.contains(s)||c!=="static"){// istanbul ignore else @preserve -!(l&&l.toString().length)&&(!e.contains(s)&&c&&(!r||a.includes(s))||n&&n.contains(s))&&(o.relatedTarget=n&&n.contains(s)?n:null,o.hide());// istanbul ignore next @preserve -r&&r.tagName==="A"&&t.preventDefault()}}},la=({code:t,target:s})=>{const e=L(Es,$(s)),n=e&&ue(e);// istanbul ignore else @preserve -if(n){// istanbul ignore else @preserve -n.options.keyboard&&t===ts&&(n.relatedTarget=null,n.hide())}},ra=t=>{const{element:s}=t;v(s,We),Ht(s,ie),M(s,Se,"true"),M(s,"role","dialog"),b(s,eo),no(t,!0),rt(s),Ie(s)},da=t=>{const{element:s,triggers:e}=t;M(s,ie,"true"),Ht(s,Se),Ht(s,"role"),A(s,{visibility:""});const n=je.relatedTarget||e.find(Vn);// istanbul ignore else @preserve -n&&rt(n),zn(s),b(s,so),v(s,We),Ie(s),Ot(s)||no(t)};class Jt extends it{constructor(e,n){super(e,n);d(this,"_toggleEventListeners",e=>{const n=e?I:N;this.triggers.forEach(o=>n(o,O,aa))});const{element:o}=this;this.triggers=[...J(Cs,$(o))].filter(i=>X(i)===o),this.relatedTarget=null,this._toggleEventListeners(!0)}get name(){return ws}get defaults(){return sa}toggle(){f(this.element,p)?this.hide():this.show()}show(){const{element:e,options:n,relatedTarget:o}=this;let i=0;if(!f(e,p)&&(je.relatedTarget=o||void 0,eo.relatedTarget=o||void 0,b(e,je),!je.defaultPrevented)){const a=Ot(e);if(a&&a!==e){const c=ue(a)||z(a,bs);// istanbul ignore else @preserve -c&&c.hide()}if(n.backdrop)$s(k)?Ts():Wn(e,!0),i=de(k),jn(),setTimeout(()=>oo(this),i);else{oo(this);// istanbul ignore next @preserve - this test was done on Modal -a&&f(k,p)&&Re()}}}hide(){const{element:e,relatedTarget:n}=this;f(e,p)&&(Ss.relatedTarget=n||void 0,so.relatedTarget=n||void 0,b(e,Ss),Ss.defaultPrevented||(h(e,We),v(e,p),ia(this)))}dispose(){const{element:e}=this,n=f(e,p),o=()=>setTimeout(()=>super.dispose(),1);if(this.hide(),this._toggleEventListeners(),n){x(e,o);// istanbul ignore next @preserve -}else o()}}d(Jt,"selector",ta),d(Jt,"init",na),d(Jt,"getInstance",ue);const Mt="popover",ze="Popover",ft="tooltip",io=t=>{const s=t===ft,e=s?`${t}-inner`:`${t}-body`,n=s?"":`

`,o=`
`,i=`
`;return`
${n+o+i}
`},ao={top:"top",bottom:"bottom",left:"start",right:"end"},Ps=t=>{const s=/\b(top|bottom|start|end)+/,{element:e,tooltip:n,container:o,options:i,arrow:a}=t;// istanbul ignore else @preserve -if(n){const c={...ao},r=wt(e);A(n,{top:"",left:"",right:"",bottom:""});const l=t.name===ze,{offsetWidth:u,offsetHeight:m}=n,{clientWidth:w,clientHeight:B,offsetWidth:gt}=bt(e);let{placement:T}=i;const{clientWidth:Y,offsetWidth:Z}=o,pt=V(o,"position")==="fixed",S=Math.abs(pt?Y-Z:w-gt),st=r&&pt?S:0,ct=w-(r?0:S)-1,{width:j,height:U,left:P,right:ne,top:Ct}=is(e,!0),{x:R,y:St}={x:P,y:Ct};A(a,{top:"",left:"",right:"",bottom:""});let Rt=0,we="",Pt=0,ks="",oe="",Ve="",Is="";const Ft=a.offsetWidth||0,Dt=a.offsetHeight||0,Ns=Ft/2;let $e=Ct-m-Dt<0,Ee=Ct+m+U+Dt>=B,Te=P-u-Ft=ct;const Xe=["left","right"],Os=["top","bottom"];$e=Xe.includes(T)?Ct+U/2-m/2-Dt<0:$e,Ee=Xe.includes(T)?Ct+m/2+U/2+Dt>=B:Ee,Te=Os.includes(T)?P+j/2-u/2=ct:ye,T=Xe.includes(T)&&Te&&ye?"top":T,T=T==="top"&&$e?"bottom":T,T=T==="bottom"&&Ee?"top":T,T=T==="left"&&Te?"right":T,T=T==="right"&&ye?"left":T,n.className.includes(T)||(n.className=n.className.replace(s,c[T]));// istanbul ignore else @preserve -Xe.includes(T)?(T==="left"?Pt=R-u-(l?Ft:0):Pt=R+j+(l?Ft:0),$e&&Ee?(Rt=0,we=0,oe=Ct+U/2-Dt/2):$e?(Rt=St,we="",oe=U/2-Ft):Ee?(Rt=St-m+U,we="",oe=m-U/2-Ft):(Rt=St-m/2+U/2,oe=m/2-Dt/2)):Os.includes(T)&&(T==="top"?Rt=St-m-(l?Dt:0):Rt=St+U+(l?Dt:0),Te?(Pt=0,Ve=R+j/2-Ns):ye?(Pt="auto",ks=0,Is=j/2+ct-ne-Ns):(Pt=R-u/2+j/2,Ve=u/2-Ns)),A(n,{top:`${Rt}px`,bottom:we===""?"":`${we}px`,left:Pt==="auto"?Pt:`${Pt}px`,right:ks!==""?`${ks}px`:""});// istanbul ignore else @preserve -y(a)&&(oe!==""&&(a.style.top=`${oe}px`),Ve!==""?a.style.left=`${Ve}px`:Is!==""&&(a.style.right=`${Is}px`));const Ma=E(`updated.bs.${jt(t.name)}`);b(e,Ma)}},Ds={template:io(ft),title:"",customClass:"",trigger:"hover focus",placement:"top",sanitizeFn:void 0,animation:!0,delay:200,container:document.body,content:"",dismissible:!1,btnClose:""},co="data-original-title",_t="Tooltip",yt=(t,s,e)=>{// istanbul ignore else @preserve -if(re(s)&&s.length){let n=s.trim();oi(e)&&(n=e(n));const i=new DOMParser().parseFromString(n,"text/html");t.append(...i.body.childNodes)}else y(s)?t.append(s):(ii(s)||ni(s)&&s.every(H))&&t.append(...s)},ha=t=>{const s=t.name===_t,{id:e,element:n,options:o}=t,{title:i,placement:a,template:c,animation:r,customClass:l,sanitizeFn:u,dismissible:m,content:w,btnClose:B}=o,gt=s?ft:Mt,T={...ao};let Y=[],Z=[];wt(n)&&(T.left="end",T.right="start");const se=`bs-${gt}-${T[a]}`;let pt;if(y(c))pt=c;else{const st=mt("div");yt(st,c,u),pt=st.firstChild}t.tooltip=y(pt)?pt.cloneNode(!0):void 0;const{tooltip:S}=t;// istanbul ignore else @preserve -if(S){M(S,"id",e),M(S,"role",ft);const st=s?`${ft}-inner`:`${Mt}-body`,ct=s?null:L(`.${Mt}-header`,S),j=L(`.${st}`,S);t.arrow=L(`.${gt}-arrow`,S);const{arrow:U}=t;if(y(i))Y=[i.cloneNode(!0)];else{const P=mt("div");yt(P,i,u),Y=[...P.childNodes]}if(y(w))Z=[w.cloneNode(!0)];else{const P=mt("div");yt(P,w,u),Z=[...P.childNodes]}if(m)if(i)if(y(B))Y=[...Y,B.cloneNode(!0)];else{const P=mt("div");yt(P,B,u),Y=[...Y,P.firstChild]}else{// istanbul ignore else @preserve -if(ct&&ct.remove(),y(B))Z=[...Z,B.cloneNode(!0)];else{const P=mt("div");yt(P,B,u),Z=[...Z,P.firstChild]}}// istanbul ignore else @preserve -if(s)i&&j&&yt(j,i,u);else{// istanbul ignore else @preserve -i&&ct&&yt(ct,Y,u);// istanbul ignore else @preserve -w&&j&&yt(j,Z,u),t.btn=L(".btn-close",S)||void 0}h(S,"position-fixed"),h(U,"position-absolute");// istanbul ignore else @preserve -f(S,gt)||h(S,gt);// istanbul ignore else @preserve -r&&!f(S,F)&&h(S,F);// istanbul ignore else @preserve -l&&!f(S,l)&&h(S,l);// istanbul ignore else @preserve -f(S,se)||h(S,se)}},fa=t=>{const s=["HTML","BODY"],e=[];let{parentNode:n}=t;for(;n&&!s.includes(n.nodeName);){n=si(n);// istanbul ignore else @preserve -qs(n)||ai(n)||e.push(n)}return e.find((o,i)=>V(o,"position")!=="relative"&&e.slice(i+1).every(a=>V(a,"position")==="static")?o:null)||$(t).body},ga=`[${at}="${ft}"],[data-tip="${ft}"]`,lo="title";let ro=t=>z(t,_t);const pa=t=>new Gt(t),ua=t=>{const{element:s,tooltip:e,container:n,offsetParent:o}=t;Ht(s,Ms),Mn(e,n===o?n:o)},me=t=>{const{tooltip:s,container:e,offsetParent:n}=t;return s&&$s(s,e===n?e:n)},ma=(t,s)=>{const{element:e}=t;t._toggleEventListeners();// istanbul ignore else @preserve -ce(e,co)&&t.name===_t&&uo(t);// istanbul ignore else @preserve -s&&s()},ho=(t,s)=>{const e=s?I:N,{element:n}=t;e($(n),Je,t.handleTouch,ot),[Ze,He].forEach(o=>{e(he(n),o,t.update,ot)})},fo=t=>{const{element:s}=t,e=E(`shown.bs.${jt(t.name)}`);ho(t,!0),b(s,e),g.clear(s,"in")},go=t=>{const{element:s}=t,e=E(`hidden.bs.${jt(t.name)}`);ho(t),ua(t),b(s,e),g.clear(s,"out")},po=(t,s)=>{const e=s?I:N,{element:n,container:o,offsetParent:i}=t,{offsetHeight:a,scrollHeight:c}=o,r=_(n,`.${q}`),l=_(n,`.${Q}`);// istanbul ignore else @preserve -const u=he(n),w=o===i&&a!==c?o:u;e(w,He,t.update,ot),e(w,Ze,t.update,ot),r&&e(r,`hide.bs.${q}`,t.handleHide),l&&e(l,`hide.bs.${Q}`,t.handleHide)},uo=(t,s)=>{const e=[co,lo],{element:n}=t;M(n,e[s?0:1],s||nt(n,e[0])||""),Ht(n,e[s?1:0])};class Gt extends it{constructor(e,n){super(e,n);d(this,"handleFocus",()=>rt(this.element));d(this,"handleShow",()=>this.show());d(this,"handleHide",()=>this.hide());d(this,"update",()=>{Ps(this)});d(this,"toggle",()=>{const{tooltip:e}=this;e&&!me(this)?this.show():this.hide()});d(this,"handleTouch",({target:e})=>{const{tooltip:n,element:o}=this;// istanbul ignore if @preserve -n&&n.contains(e)||e===o||e&&o.contains(e)||this.hide()});d(this,"_toggleEventListeners",e=>{const n=e?I:N,{element:o,options:i,btn:a}=this,{trigger:c}=i,l=!!(this.name!==_t&&i.dismissible);// istanbul ignore else @preserve -c.includes("manual")||(this.enabled=!!e,c.split(" ").forEach(m=>{// istanbul ignore else @preserve -if(m===_o){n(o,Rs,this.handleShow),n(o,De,this.handleShow);// istanbul ignore else @preserve -l||(n(o,qe,this.handleHide),n($(o),Je,this.handleTouch,ot))}else if(m===O)n(o,m,l?this.handleShow:this.toggle);else if(m===Ye){n(o,Ue,this.handleShow);// istanbul ignore else @preserve -l||n(o,Bs,this.handleHide);// istanbul ignore else @preserve -Yo&&n(o,O,this.handleFocus)}// istanbul ignore else @preserve -l&&a&&n(a,O,this.handleHide)}))});const{element:o}=this,i=this.name===_t,a=i?ft:Mt,c=i?_t:ze;// istanbul ignore next @preserve: this is to set Popover too -ro=l=>z(l,c),this.enabled=!0,this.id=`${a}-${Gs(o,a)}`;const{options:r}=this;if(!(!r.title&&i||!i&&!r.content)){lt(Ds,{titleAttr:""});// istanbul ignore else @preserve -ce(o,lo)&&i&&typeof r.title=="string"&&uo(this,r.title),this.container=fa(o),this.offsetParent=["sticky","fixed"].some(l=>V(this.container,"position")===l)?this.container:$(this.element).body,ha(this),this._toggleEventListeners(!0)}}get name(){return _t}get defaults(){return Ds}show(){const{options:e,tooltip:n,element:o,container:i,offsetParent:a,id:c}=this,{animation:r}=e,l=g.get(o,"out"),u=i===a?i:a;g.clear(o,"out"),n&&!l&&!me(this)&&g.set(o,()=>{const m=E(`show.bs.${jt(this.name)}`);b(o,m);// istanbul ignore else @preserve -if(!m.defaultPrevented){On(n,u),M(o,Ms,`#${c}`),this.update(),po(this,!0);// istanbul ignore else @preserve -f(n,p)||h(n,p);// istanbul ignore else @preserve -r?x(n,()=>fo(this)):fo(this)}},17,"in")}hide(){const{options:e,tooltip:n,element:o}=this,{animation:i,delay:a}=e;g.clear(o,"in");// istanbul ignore else @preserve -n&&me(this)&&g.set(o,()=>{const c=E(`hide.bs.${jt(this.name)}`);b(o,c);// istanbul ignore else @preserve -if(!c.defaultPrevented){this.update(),v(n,p),po(this);// istanbul ignore else @preserve -i?x(n,()=>go(this)):go(this)}},a+17,"out")}enable(){const{enabled:e}=this;// istanbul ignore else @preserve -e||(this._toggleEventListeners(!0),this.enabled=!e)}disable(){const{tooltip:e,enabled:n}=this;// istanbul ignore else @preserve -n&&(e&&me(this)&&this.hide(),this._toggleEventListeners(),this.enabled=!n)}toggleEnabled(){this.enabled?this.disable():this.enable()}dispose(){const{tooltip:e,options:n}=this,o={...this,name:this.name},i=()=>setTimeout(()=>ma(o,()=>super.dispose()),17);n.animation&&me(o)?(this.options.delay=0,this.hide(),x(e,i)):i()}}d(Gt,"selector",ga),d(Gt,"init",pa),d(Gt,"getInstance",ro),d(Gt,"styleTip",Ps);const va=`[${at}="${Mt}"],[data-tip="${Mt}"]`,ba=lt({},Ds,{template:io(Mt),content:"",dismissible:!1,btnClose:''}),wa=t=>z(t,ze),$a=t=>new Bt(t);class Bt extends Gt{constructor(e,n){super(e,n);d(this,"show",()=>{super.show();const{options:e,btn:n}=this;// istanbul ignore else @preserve -e.dismissible&&n&&setTimeout(()=>rt(n),17)})}get name(){return ze}get defaults(){return ba}}d(Bt,"selector",va),d(Bt,"init",$a),d(Bt,"getInstance",wa),d(Bt,"styleTip",Ps);const ve="tab",mo="Tab",vo=`[${at}="${ve}"]`,bo=t=>z(t,mo),Ea=t=>new Qt(t),Hs=E(`show.bs.${ve}`),wo=E(`shown.bs.${ve}`),xs=E(`hide.bs.${ve}`),$o=E(`hidden.bs.${ve}`),be=new Map,Eo=t=>{const{tabContent:s,nav:e}=t;// istanbul ignore else @preserve -s&&f(s,It)&&(s.style.height="",v(s,It));// istanbul ignore else @preserve -e&&g.clear(e)},To=t=>{const{element:s,tabContent:e,content:n,nav:o}=t,{tab:i}=y(o)&&be.get(o)||{tab:null};// istanbul ignore else @preserve -if(e&&n&&f(n,F)){const{currentHeight:a,nextHeight:c}=be.get(s)||{currentHeight:0,nextHeight:0};// istanbul ignore else @preserve: vitest won't validate this branch -a!==c?setTimeout(()=>{e.style.height=`${c}px`,At(e),x(e,()=>Eo(t))},50):Eo(t)}else o&&g.clear(o);wo.relatedTarget=i,b(s,wo)},yo=t=>{const{element:s,content:e,tabContent:n,nav:o}=t,{tab:i,content:a}=o&&be.get(o)||{tab:null,content:null};let c=0;// istanbul ignore else @preserve -n&&e&&f(e,F)&&([a,e].forEach(r=>{// istanbul ignore else @preserve -y(r)&&h(r,"overflow-hidden")}),c=y(a)?a.scrollHeight:0),Hs.relatedTarget=i,$o.relatedTarget=s,b(s,Hs);// istanbul ignore else @preserve -if(!Hs.defaultPrevented){// istanbul ignore else @preserve -e&&h(e,C);// istanbul ignore else @preserve -a&&v(a,C);// istanbul ignore else @preserve -if(n&&e&&f(e,F)){const r=e.scrollHeight;be.set(s,{currentHeight:c,nextHeight:r,tab:null,content:null}),h(n,It),n.style.height=`${c}px`,At(n),[a,e].forEach(l=>{// istanbul ignore else @preserve -l&&v(l,"overflow-hidden")})}if(e&&e&&f(e,F))setTimeout(()=>{h(e,p),x(e,()=>{To(t)})},1);else{// istanbul ignore else @preserve -e&&h(e,p),To(t)}// istanbul ignore else @preserve -i&&b(i,$o)}},Co=t=>{const{nav:s}=t;// istanbul ignore next @preserve -if(!y(s))return{tab:null,content:null};const e=dt(C,s);let n=null;// istanbul ignore else @preserve -e.length===1&&!Nt.some(i=>f(e[0].parentElement,i))?[n]=e:e.length>1&&(n=e[e.length-1]);const o=y(n)?X(n):null;return{tab:n,content:o}},So=t=>{// istanbul ignore next @preserve -if(!y(t))return null;const s=_(t,`.${Nt.join(",.")}`);return s?L(`.${Nt[0]}-toggle`,s):null},Ta=t=>{const s=bo(t.target);// istanbul ignore else @preserve -s&&(t.preventDefault(),s.show())};class Qt extends it{constructor(e){super(e);d(this,"_toggleEventListeners",e=>{(e?I:N)(this.element,O,Ta)});const{element:n}=this,o=X(n);// istanbul ignore else @preserve -if(o){const i=_(n,".nav"),a=_(o,".tab-content");this.nav=i,this.content=o,this.tabContent=a,this.dropdown=So(n);const{tab:c}=Co(this);if(i&&!c){const r=L(vo,i),l=r&&X(r);// istanbul ignore else @preserve -l&&(h(r,C),h(l,p),h(l,C),M(n,Ke,"true"))}this._toggleEventListeners(!0)}}get name(){return mo}show(){const{element:e,content:n,nav:o,dropdown:i}=this;// istanbul ignore else @preserve -if(!(o&&g.get(o))&&!f(e,C)){const{tab:a,content:c}=Co(this);// istanbul ignore else @preserve -o&&be.set(o,{tab:a,content:c,currentHeight:0,nextHeight:0}),xs.relatedTarget=e;// istanbul ignore else @preserve -if(y(a)){b(a,xs);// istanbul ignore else @preserve -if(!xs.defaultPrevented){h(e,C),M(e,Ke,"true");const r=y(a)&&So(a);r&&f(r,C)&&v(r,C);// istanbul ignore else @preserve -if(o){const l=()=>{// istanbul ignore else @preserve -a&&(v(a,C),M(a,Ke,"false")),i&&!f(i,C)&&h(i,C)};c&&(f(c,F)||n&&f(n,F))?g.set(o,l,1):l()}// istanbul ignore else @preserve -c&&(v(c,p),f(c,F)?x(c,()=>yo(this)):yo(this))}}}}dispose(){this._toggleEventListeners(),super.dispose()}}d(Qt,"selector",vo),d(Qt,"init",Ea),d(Qt,"getInstance",bo);const tt="toast",Po="Toast",ya=`.${tt}`,Ca=`[${Ne}="${tt}"]`,Do=`[${at}="${tt}"]`,te="showing",Ho="hide",Sa={animation:!0,autohide:!0,delay:5e3},As=t=>z(t,Po),Pa=t=>new ee(t),xo=E(`show.bs.${tt}`),Da=E(`shown.bs.${tt}`),Ao=E(`hide.bs.${tt}`),Ha=E(`hidden.bs.${tt}`),Lo=t=>{const{element:s,options:e}=t;v(s,te),g.clear(s,te),b(s,Da);// istanbul ignore else @preserve -e.autohide&&g.set(s,()=>t.hide(),e.delay,tt)},ko=t=>{const{element:s}=t;v(s,te),v(s,p),h(s,Ho),g.clear(s,tt),b(s,Ha)},xa=t=>{const{element:s,options:e}=t;h(s,te),e.animation?(At(s),x(s,()=>ko(t))):ko(t)},Aa=t=>{const{element:s,options:e}=t;g.set(s,()=>{v(s,Ho),At(s),h(s,p),h(s,te),e.animation?x(s,()=>Lo(t)):Lo(t)},17,te)},La=t=>{const{target:s}=t,e=s&&_(s,Do),n=e&&X(e),o=n&&As(n);// istanbul ignore else @preserve -if(o){// istanbul ignore else @preserve -e&&e.tagName==="A"&&t.preventDefault(),o.relatedTarget=e,o.show()}},ka=t=>{const s=t.target,e=As(s),{type:n,relatedTarget:o}=t;// istanbul ignore else @preserve: a solid filter is required -e&&s!==o&&!s.contains(o)&&([De,Ue].includes(n)?g.clear(s,tt):g.set(s,()=>e.hide(),e.options.delay,tt))};class ee extends it{constructor(e,n){super(e,n);d(this,"show",()=>{const{element:e,isShown:n}=this;// istanbul ignore else @preserve -e&&!n&&(b(e,xo),xo.defaultPrevented||Aa(this))});d(this,"hide",()=>{const{element:e,isShown:n}=this;// istanbul ignore else @preserve -e&&n&&(b(e,Ao),Ao.defaultPrevented||xa(this))});d(this,"_toggleEventListeners",e=>{const n=e?I:N,{element:o,triggers:i,dismiss:a,options:c,hide:r}=this;// istanbul ignore else @preserve -a&&n(a,O,r);// istanbul ignore else @preserve -c.autohide&&[Ue,Bs,De,qe].forEach(l=>n(o,l,ka));// istanbul ignore else @preserve -i.length&&i.forEach(l=>n(l,O,La))});const{element:o,options:i}=this;i.animation&&!f(o,F)?h(o,F):!i.animation&&f(o,F)&&v(o,F),this.dismiss=L(Ca,o),this.triggers=[...J(Do,$(o))].filter(a=>X(a)===o),this._toggleEventListeners(!0)}get name(){return Po}get defaults(){return Sa}get isShown(){return f(this.element,p)}dispose(){const{element:e,isShown:n}=this;this._toggleEventListeners(),g.clear(e,tt),n&&v(e,p),super.dispose()}}d(ee,"selector",ya),d(ee,"init",Pa),d(ee,"getInstance",As);const Io={Alert:Vt,Button:Xt,Carousel:Kt,Collapse:Yt,Dropdown:qt,Modal:Zt,Offcanvas:Jt,Popover:Bt,Tab:Qt,Toast:ee},Ia=(t,s)=>{[...s].forEach(e=>t(e))},Na=(t,s)=>{const e=xt.getAllFor(t);e&&[...e].forEach(([n,o])=>{s.contains(n)&&o.dispose()})},Ls=t=>{const s=t&&t.nodeName?t:document,e=[...ci("*",s)];Go(Io).forEach(n=>{const{init:o,selector:i}=n;Ia(o,e.filter(a=>tn(a,i)))})},Oa=t=>{const s=t&&t.nodeName?t:document;ss(Io).forEach(e=>{Na(e,s)})};return document.body?Ls():I(document,"DOMContentLoaded",()=>Ls(),{once:!0}),D.Alert=Vt,D.Button=Xt,D.Carousel=Kt,D.Collapse=Yt,D.Dropdown=qt,D.Listener=No,D.Modal=Zt,D.Offcanvas=Jt,D.Popover=Bt,D.Tab=Qt,D.Toast=ee,D.initCallback=Ls,D.removeDataAPI=Oa,Object.defineProperty(D,Symbol.toStringTag,{value:"Module"}),D}({}); +var BSN=function(R){"use strict";const Ln="aria-describedby",ue="aria-expanded",Xt="aria-hidden",ge="aria-modal",_n="aria-pressed",ke="aria-selected",Le="focus",_e="focusin",In="focusout",pe="keydown",Ao="keyup",k="click",On="mousedown",ko="hover",me="mouseenter",Ie="mouseleave",Lo="pointerdown",_o="pointermove",Io="pointerup",Oe="touchstart",Oo="dragstart",No='a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]',Ne="ArrowDown",Me="ArrowUp",Nn="ArrowLeft",Mn="ArrowRight",Be="Escape",Mo="transitionDuration",Bo="transitionDelay",Re="transitionend",Bn="transitionProperty",Ro=()=>{const e=/(iPhone|iPod|iPad)/;return navigator?.userAgentData?.brands.some(t=>e.test(t.brand))||e.test(navigator?.userAgent)||!1},ve=()=>{},Wo=(e,t,n,s)=>{const o=s||!1;e.addEventListener(t,n,o)},Fo=(e,t,n,s)=>{const o=s||!1;e.removeEventListener(t,n,o)},tt=(e,t)=>e.getAttribute(t),Yt=(e,t)=>e.hasAttribute(t),L=(e,t,n)=>e.setAttribute(t,n),yt=(e,t)=>e.removeAttribute(t),d=(e,...t)=>{e.classList.add(...t)},p=(e,...t)=>{e.classList.remove(...t)},h=(e,t)=>e.classList.contains(t),Ut=e=>e!=null&&typeof e=="object"||!1,x=e=>Ut(e)&&typeof e.nodeType=="number"&&[1,2,3,4,5,6,7,8,9,10,11].some(t=>e.nodeType===t)||!1,X=e=>x(e)&&e.nodeType===1||!1,Bt=new Map,Ct={data:Bt,set:(e,t,n)=>{X(e)&&(Bt.has(t)||Bt.set(t,new Map),Bt.get(t).set(e,n))},getAllFor:e=>Bt.get(e)||null,get:(e,t)=>{if(!X(e)||!t)return null;const n=Ct.getAllFor(t);return e&&n&&n.get(e)||null},remove:(e,t)=>{const n=Ct.getAllFor(t);!n||!X(e)||(n.delete(e),n.size===0&&Bt.delete(t))}},W=(e,t)=>Ct.get(e,t),Rn=e=>e?.trim().replace(/(?:^\w|[A-Z]|\b\w)/g,(t,n)=>n===0?t.toLowerCase():t.toUpperCase()).replace(/\s+/g,""),Gt=e=>typeof e=="string"||!1,Wn=e=>Ut(e)&&e.constructor.name==="Window"||!1,Fn=e=>x(e)&&e.nodeType===9||!1,w=e=>Fn(e)?e:x(e)?e.ownerDocument:Wn(e)?e.document:globalThis.document,ot=(e,...t)=>Object.assign(e,...t),ht=e=>{if(!e)return;if(Gt(e))return w().createElement(e);const{tagName:t}=e,n=ht(t);if(!n)return;const s={...e};return delete s.tagName,ot(n,s)},b=(e,t)=>e.dispatchEvent(t),N=(e,t,n)=>{const s=getComputedStyle(e,n),o=t.replace("webkit","Webkit").replace(/([A-Z])/g,"-$1").toLowerCase();return s.getPropertyValue(o)},jo=e=>{const t=N(e,Bn),n=N(e,Bo),s=n.includes("ms")?1:1e3,o=t&&t!=="none"?parseFloat(n)*s:0;return Number.isNaN(o)?0:o},Qt=e=>{const t=N(e,Bn),n=N(e,Mo),s=n.includes("ms")?1:1e3,o=t&&t!=="none"?parseFloat(n)*s:0;return Number.isNaN(o)?0:o},S=(e,t)=>{let n=0;const s=new Event(Re),o=Qt(e),i=jo(e);if(o){const r=a=>{a.target===e&&(t.apply(e,[a]),e.removeEventListener(Re,r),n=1)};e.addEventListener(Re,r),setTimeout(()=>{n||b(e,s)},o+i+17)}else t.apply(e,[s])},it=(e,t)=>e.focus(t),jn=e=>["true",!0].includes(e)?!0:["false",!1].includes(e)?!1:["null","",null,void 0].includes(e)?null:e!==""&&!Number.isNaN(+e)?+e:e,be=e=>Object.entries(e),zo=(e,t,n,s)=>{if(!X(e))return t;const o={...n},i={...e.dataset},r={...t},a={},c="title";return be(i).forEach(([l,g])=>{const v=typeof l=="string"&&l.includes(s)?Rn(l.replace(s,"")):Rn(l);a[v]=jn(g)}),be(o).forEach(([l,g])=>{o[l]=jn(g)}),be(t).forEach(([l,g])=>{l in o?r[l]=o[l]:l in a?r[l]=a[l]:r[l]=l===c?tt(e,c):g}),r},zn=e=>Object.keys(e),E=(e,t)=>{const n=new CustomEvent(e,{cancelable:!0,bubbles:!0});return Ut(t)&&ot(n,t),n},Zt={passive:!0},St=e=>e.offsetHeight,P=(e,t)=>{be(t).forEach(([n,s])=>{if(s&&Gt(n)&&n.includes("--"))e.style.setProperty(n,s);else{const o={};o[n]=s,ot(e.style,o)}})},We=e=>Ut(e)&&e.constructor.name==="Map"||!1,Ko=e=>typeof e=="number"||!1,ft=new Map,f={set:(e,t,n,s)=>{X(e)&&(s&&s.length?(ft.has(e)||ft.set(e,new Map),ft.get(e).set(s,setTimeout(t,n))):ft.set(e,setTimeout(t,n)))},get:(e,t)=>{if(!X(e))return null;const n=ft.get(e);return t&&n&&We(n)?n.get(t)||null:Ko(n)?n:null},clear:(e,t)=>{if(!X(e))return;const n=ft.get(e);t&&t.length&&We(n)?(clearTimeout(n.get(t)),n.delete(t),n.size===0&&ft.delete(e)):(clearTimeout(n),ft.delete(e))}},Jt=e=>e.toLowerCase(),Y=(e,t)=>(x(t)?t:w()).querySelectorAll(e),Fe=new Map;function Vo(e){const{shiftKey:t,code:n}=e,s=w(this),o=[...Y(No,this)].filter(a=>!Yt(a,"disabled")&&!tt(a,Xt));if(!o.length)return;const i=o[0],r=o[o.length-1];n==="Tab"&&(t&&s.activeElement===i?(r.focus(),e.preventDefault()):!t&&s.activeElement===r&&(i.focus(),e.preventDefault()))}const qo=e=>Fe.has(e)===!0,we=e=>{const t=qo(e);(t?Fo:Wo)(e,"keydown",Vo),t?Fe.delete(e):Fe.set(e,!0)},D=e=>X(e)&&"offsetWidth"in e||!1,Rt=(e,t)=>{const{width:n,height:s,top:o,right:i,bottom:r,left:a}=e.getBoundingClientRect();let c=1,l=1;if(t&&D(e)){const{offsetWidth:g,offsetHeight:v}=e;c=g>0?Math.round(n)/g:1,l=v>0?Math.round(s)/v:1}return{width:n/c,height:s/l,top:o/l,right:i/c,bottom:r/l,left:a/c,x:a/c,y:o/l}},Pt=e=>w(e).body,ut=e=>w(e).documentElement,Xo=e=>{const t=Wn(e),n=t?e.scrollX:e.scrollLeft,s=t?e.scrollY:e.scrollTop;return{x:n,y:s}},Kn=e=>x(e)&&e.constructor.name==="ShadowRoot"||!1,Yo=e=>e.nodeName==="HTML"?e:X(e)&&e.assignedSlot||x(e)&&e.parentNode||Kn(e)&&e.host||ut(e),Vn=e=>e?Fn(e)?e.defaultView:x(e)?e?.ownerDocument?.defaultView:e:window,Uo=e=>x(e)&&["TABLE","TD","TH"].includes(e.nodeName)||!1,qn=(e,t)=>e.matches(t),Go=e=>{if(!D(e))return!1;const{width:t,height:n}=Rt(e),{offsetWidth:s,offsetHeight:o}=e;return Math.round(t)!==s||Math.round(n)!==o},Qo=(e,t,n)=>{const s=D(t),o=Rt(e,s&&Go(t)),i={x:0,y:0};if(s){const r=Rt(t,!0);i.x=r.x+t.clientLeft,i.y=r.y+t.clientTop}return{x:o.left+n.x-i.x,y:o.top+n.y-i.y,width:o.width,height:o.height}};let Xn=0,Yn=0;const Wt=new Map,Un=(e,t)=>{let n=t?Xn:Yn;if(t){const s=Un(e),o=Wt.get(s)||new Map;Wt.has(s)||Wt.set(s,o),We(o)&&!o.has(t)?(o.set(t,n),Xn+=1):n=o.get(t)}else{const s=e.id||e;Wt.has(s)?n=Wt.get(s):(Wt.set(s,n),Yn+=1)}return n},Zo=e=>Array.isArray(e)||!1,Gn=e=>{if(!x(e))return!1;const{top:t,bottom:n}=Rt(e),{clientHeight:s}=ut(e);return t<=s&&n>=0},Jo=e=>typeof e=="function"||!1,ti=e=>Ut(e)&&e.constructor.name==="NodeList"||!1,gt=e=>ut(e).dir==="rtl",M=(e,t)=>!e||!t?null:e.closest(t)||M(e.getRootNode().host,t)||null,H=(e,t)=>X(e)?e:(X(t)?t:w()).querySelector(e),ei=(e,t)=>(x(t)?t:w()).getElementsByTagName(e),rt=(e,t)=>(t&&x(t)?t:w()).getElementsByClassName(e),Ft={},Qn=e=>{const{type:t,currentTarget:n}=e;Ft[t].forEach((s,o)=>{n===o&&s.forEach((i,r)=>{r.apply(o,[e]),typeof i=="object"&&i.once&&I(o,t,r,i)})})},_=(e,t,n,s)=>{Ft[t]||(Ft[t]=new Map);const o=Ft[t];o.has(e)||o.set(e,new Map);const i=o.get(e),{size:r}=i;i.set(n,s),r||e.addEventListener(t,Qn,s)},I=(e,t,n,s)=>{const o=Ft[t],i=o&&o.get(e),r=i&&i.get(n),a=r!==void 0?r:s;i&&i.has(n)&&i.delete(n),o&&(!i||!i.size)&&o.delete(e),(!o||!o.size)&&delete Ft[t],(!i||!i.size)&&e.removeEventListener(t,Qn,a)},O="fade",u="show",Ee="data-bs-dismiss",$e="alert",Zn="Alert",at=e=>h(e,"disabled")||tt(e,"disabled")==="true",ni="5.1.0";class et{constructor(t,n){let s;try{if(X(t))s=t;else if(Gt(t)){if(s=H(t),!s)throw Error(`"${t}" is not a valid selector.`)}else throw Error("your target is not an instance of HTMLElement.")}catch(i){throw Error(`${this.name} Error: ${i.message}`)}const o=Ct.get(s,this.name);o&&o._toggleEventListeners(),this.element=s,this.options=this.defaults&&zn(this.defaults).length?zo(s,this.defaults,n||{},"bs"):{},Ct.set(s,this.name,this)}get version(){return ni}get name(){return"BaseComponent"}get defaults(){return{}}_toggleEventListeners=()=>{};dispose(){Ct.remove(this.element,this.name),zn(this).forEach(t=>{delete this[t]})}}const si=`.${$e}`,oi=`[${Ee}="${$e}"]`,ii=e=>W(e,Zn),ri=e=>new je(e),Jn=E(`close.bs.${$e}`),ai=E(`closed.bs.${$e}`),ts=e=>{const{element:t}=e;b(t,ai),e._toggleEventListeners(),e.dispose(),t.remove()};class je extends et{static selector=si;static init=ri;static getInstance=ii;dismiss;constructor(t){super(t),this.dismiss=H(oi,this.element),this._toggleEventListeners(!0)}get name(){return Zn}close=t=>{const{element:n,dismiss:s}=this;!n||!h(n,u)||t&&s&&at(s)||(b(n,Jn),!Jn.defaultPrevented&&(p(n,u),h(n,O)?S(n,()=>ts(this)):ts(this)))};_toggleEventListeners=t=>{const n=t?_:I,{dismiss:s,close:o}=this;s&&n(s,k,o)};dispose(){this._toggleEventListeners(),super.dispose()}}const y="active",nt="data-bs-toggle",ci="button",es="Button",li=`[${nt}="${ci}"]`,di=e=>W(e,es),hi=e=>new ze(e);class ze extends et{static selector=li;static init=hi;static getInstance=di;constructor(t){super(t);const{element:n}=this;this.isActive=h(n,y),L(n,_n,String(!!this.isActive)),this._toggleEventListeners(!0)}get name(){return es}toggle=t=>{t&&t.preventDefault();const{element:n,isActive:s}=this;if(at(n))return;(s?p:d)(n,y),L(n,_n,s?"false":"true"),this.isActive=h(n,y)};_toggleEventListeners=t=>{(t?_:I)(this.element,k,this.toggle)};dispose(){this._toggleEventListeners(),super.dispose()}}const Ke="data-bs-target",xt="carousel",ns="Carousel",ss="data-bs-parent",fi="data-bs-container",F=e=>{const t=[Ke,ss,fi,"href"],n=w(e);return t.map(s=>{const o=tt(e,s);return o?s===ss?M(e,o):H(o,n):null}).filter(s=>s)[0]},te=`[data-bs-ride="${xt}"]`,Q=`${xt}-item`,Ve="data-bs-slide-to",pt="data-bs-slide",mt="paused",os={pause:"hover",keyboard:!1,touch:!0,interval:5e3},ct=e=>W(e,ns),ui=e=>new Qe(e);let ee=0,Te=0,qe=0;const Xe=E(`slide.bs.${xt}`),Ye=E(`slid.bs.${xt}`),is=e=>{const{index:t,direction:n,element:s,slides:o,options:i}=e;if(e.isAnimating){const r=Ge(e),a=n==="left"?"next":"prev",c=n==="left"?"start":"end";d(o[t],y),p(o[t],`${Q}-${a}`),p(o[t],`${Q}-${c}`),p(o[r],y),p(o[r],`${Q}-${c}`),b(s,Ye),f.clear(s,pt),e.cycle&&!w(s).hidden&&i.interval&&!e.isPaused&&e.cycle()}};function gi(){const e=ct(this);e&&!e.isPaused&&!f.get(this,mt)&&d(this,mt)}function pi(){const e=ct(this);e&&e.isPaused&&!f.get(this,mt)&&e.cycle()}function mi(e){e.preventDefault();const t=M(this,te)||F(this),n=t&&ct(t);if(at(this)||!n||n.isAnimating)return;const s=+(tt(this,Ve)||0);this&&!h(this,y)&&!Number.isNaN(s)&&n.to(s)}function vi(e){e.preventDefault();const t=M(this,te)||F(this),n=t&&ct(t);if(at(this)||!n||n.isAnimating)return;const s=tt(this,pt);s==="next"?n.next():s==="prev"&&n.prev()}const bi=({code:e,target:t})=>{const n=w(t),[s]=[...Y(te,n)].filter(c=>Gn(c)),o=ct(s);if(!o||o.isAnimating||/textarea|input|select/i.test(t.nodeName))return;const i=gt(s);e===(i?Mn:Nn)?o.prev():e===(i?Nn:Mn)&&o.next()};function rs(e){const{target:t}=e,n=ct(this);n&&n.isTouch&&(n.indicator&&!n.indicator.contains(t)||!n.controls.includes(t))&&(e.stopImmediatePropagation(),e.stopPropagation(),e.preventDefault())}function wi(e){const{target:t}=e,n=ct(this);if(!n||n.isAnimating||n.isTouch)return;const{controls:s,indicators:o}=n;[...s,...o].every(i=>i===t||i.contains(t))||(ee=e.pageX,this.contains(t)&&(n.isTouch=!0,as(n,!0)))}const Ei=e=>{Te=e.pageX},$i=e=>{const{target:t}=e,n=w(t),s=[...Y(te,n)].map(a=>ct(a)).find(a=>a.isTouch);if(!s)return;const{element:o,index:i}=s,r=gt(o);qe=e.pageX,s.isTouch=!1,as(s),!n.getSelection()?.toString().length&&o.contains(t)&&Math.abs(ee-qe)>120&&(Teee&&s.to(i+(r?1:-1))),ee=0,Te=0,qe=0},Ue=(e,t)=>{const{indicators:n}=e;[...n].forEach(s=>p(s,y)),e.indicators[t]&&d(n[t],y)},as=(e,t)=>{const{element:n}=e,s=t?_:I;s(w(n),_o,Ei,Zt),s(w(n),Io,$i,Zt)},Ge=e=>{const{slides:t,element:n}=e,s=H(`.${Q}.${y}`,n);return s?[...t].indexOf(s):-1};class Qe extends et{static selector=te;static init=ui;static getInstance=ct;constructor(t,n){super(t,n);const{element:s}=this;this.direction=gt(s)?"right":"left",this.isTouch=!1,this.slides=rt(Q,s);const{slides:o}=this;if(o.length<2)return;const i=Ge(this),r=[...o].find(l=>qn(l,`.${Q}-next`));this.index=i;const a=w(s);this.controls=[...Y(`[${pt}]`,s),...Y(`[${pt}][${Ke}="#${s.id}"]`,a)].filter((l,g,v)=>g===v.indexOf(l)),this.indicator=H(`.${xt}-indicators`,s),this.indicators=[...this.indicator?Y(`[${Ve}]`,this.indicator):[],...Y(`[${Ve}][${Ke}="#${s.id}"]`,a)].filter((l,g,v)=>g===v.indexOf(l));const{options:c}=this;this.options.interval=c.interval===!0?os.interval:c.interval,r?this.index=[...o].indexOf(r):i<0&&(this.index=0,d(o[0],y),this.indicators.length&&Ue(this,0)),this.indicators.length&&Ue(this,this.index),this._toggleEventListeners(!0),c.interval&&this.cycle()}get name(){return ns}get defaults(){return os}get isPaused(){return h(this.element,mt)}get isAnimating(){return H(`.${Q}-next,.${Q}-prev`,this.element)!==null}cycle(){const{element:t,options:n,isPaused:s,index:o}=this;f.clear(t,xt),s&&(f.clear(t,mt),p(t,mt)),f.set(t,()=>{this.element&&!this.isPaused&&!this.isTouch&&Gn(t)&&this.to(o+1)},n.interval,xt)}pause(){const{element:t,options:n}=this;this.isPaused||!n.interval||(d(t,mt),f.set(t,()=>{},1,mt))}next(){this.isAnimating||this.to(this.index+1)}prev(){this.isAnimating||this.to(this.index-1)}to(t){const{element:n,slides:s,options:o}=this,i=Ge(this),r=gt(n);let a=t;if(this.isAnimating||i===a||f.get(n,pt))return;ia||i===s.length-1&&a===0)&&(this.direction=r?"left":"right");const{direction:c}=this;a<0?a=s.length-1:a>=s.length&&(a=0);const l=c==="left"?"next":"prev",g=c==="left"?"start":"end",v={relatedTarget:s[a],from:i,to:a,direction:c};ot(Xe,v),ot(Ye,v),b(n,Xe),!Xe.defaultPrevented&&(this.index=a,Ue(this,a),Qt(s[a])&&h(n,"slide")?f.set(n,()=>{d(s[a],`${Q}-${l}`),St(s[a]),d(s[a],`${Q}-${g}`),d(s[i],`${Q}-${g}`),S(s[a],()=>this.slides&&this.slides.length&&is(this))},0,pt):(d(s[a],y),p(s[i],y),f.set(n,()=>{f.clear(n,pt),n&&o.interval&&!this.isPaused&&this.cycle(),b(n,Ye)},0,pt)))}_toggleEventListeners=t=>{const{element:n,options:s,slides:o,controls:i,indicators:r}=this,{touch:a,pause:c,interval:l,keyboard:g}=s,v=t?_:I;c&&l&&(v(n,me,gi),v(n,Ie,pi)),a&&o.length>2&&(v(n,Lo,wi,Zt),v(n,Oe,rs,{passive:!1}),v(n,Oo,rs,{passive:!1})),i.length&&i.forEach(T=>{v(T,k,vi)}),r.length&&r.forEach(T=>{v(T,k,mi)}),g&&v(w(n),pe,bi)};dispose(){const{isAnimating:t}=this,n={...this,isAnimating:t};this._toggleEventListeners(),super.dispose(),n.isAnimating&&S(n.slides[n.index],()=>{is(n)})}}const Dt="collapsing",j="collapse",cs="Collapse",Ti=`.${j}`,ls=`[${nt}="${j}"]`,yi={parent:null},ye=e=>W(e,cs),Ci=e=>new Ze(e),ds=E(`show.bs.${j}`),Si=E(`shown.bs.${j}`),hs=E(`hide.bs.${j}`),Pi=E(`hidden.bs.${j}`),xi=e=>{const{element:t,parent:n,triggers:s}=e;b(t,ds),ds.defaultPrevented||(f.set(t,ve,17),n&&f.set(n,ve,17),d(t,Dt),p(t,j),P(t,{height:`${t.scrollHeight}px`}),S(t,()=>{f.clear(t),n&&f.clear(n),s.forEach(o=>L(o,ue,"true")),p(t,Dt),d(t,j),d(t,u),P(t,{height:""}),b(t,Si)}))},fs=e=>{const{element:t,parent:n,triggers:s}=e;b(t,hs),hs.defaultPrevented||(f.set(t,ve,17),n&&f.set(n,ve,17),P(t,{height:`${t.scrollHeight}px`}),p(t,j),p(t,u),d(t,Dt),St(t),P(t,{height:"0px"}),S(t,()=>{f.clear(t),n&&f.clear(n),s.forEach(o=>L(o,ue,"false")),p(t,Dt),d(t,j),P(t,{height:""}),b(t,Pi)}))},Di=e=>{const{target:t}=e,n=t&&M(t,ls),s=n&&F(n),o=s&&ye(s);n&&at(n)||o&&(o.toggle(),n?.tagName==="A"&&e.preventDefault())};class Ze extends et{static selector=Ti;static init=Ci;static getInstance=ye;constructor(t,n){super(t,n);const{element:s,options:o}=this,i=w(s);this.triggers=[...Y(ls,i)].filter(r=>F(r)===s),this.parent=D(o.parent)?o.parent:Gt(o.parent)?F(s)||H(o.parent,i):null,this._toggleEventListeners(!0)}get name(){return cs}get defaults(){return yi}hide(){const{triggers:t,element:n}=this;f.get(n)||(fs(this),t.length&&t.forEach(s=>d(s,`${j}d`)))}show(){const{element:t,parent:n,triggers:s}=this;let o,i;n&&(o=[...Y(`.${j}.${u}`,n)].find(r=>ye(r)),i=o&&ye(o)),(!n||!f.get(n))&&!f.get(t)&&(i&&o!==t&&(fs(i),i.triggers.forEach(r=>{d(r,`${j}d`)})),xi(this),s.length&&s.forEach(r=>p(r,`${j}d`)))}toggle(){h(this.element,u)?this.hide():this.show()}_toggleEventListeners=t=>{const n=t?_:I,{triggers:s}=this;s.length&&s.forEach(o=>{n(o,k,Di)})};dispose(){this._toggleEventListeners(),super.dispose()}}const Hi=e=>e!=null&&typeof e=="object"||!1,Ai=e=>Hi(e)&&typeof e.nodeType=="number"&&[1,2,3,4,5,6,7,8,9,10,11].some(t=>e.nodeType===t)||!1,us=e=>Ai(e)&&e.nodeType===1||!1,ki=e=>typeof e=="function"||!1,Li="1.0.2",gs="PositionObserver Error";class ps{entries;static version=Li;_tick;_root;_callback;constructor(t,n){if(!ki(t))throw new Error(`${gs}: ${t} is not a function.`);this.entries=new Map,this._callback=t,this._root=us(n?.root)?n.root:document?.documentElement,this._tick=0}observe=t=>{if(!us(t))throw new Error(`${gs}: ${t} is not an instance of Element.`);this._root.contains(t)&&this._new(t).then(n=>{n&&!this.getEntry(t)&&this.entries.set(t,n),this._tick||(this._tick=requestAnimationFrame(this._runCallback))})};unobserve=t=>{this.entries.has(t)&&this.entries.delete(t)};_runCallback=()=>{if(!this.entries.size)return;const t=new Promise(n=>{const s=[];this.entries.forEach(({target:o,boundingClientRect:i})=>{this._root.contains(o)&&this._new(o).then(({boundingClientRect:r,isIntersecting:a})=>{if(!a)return;const{left:c,top:l,bottom:g,right:v}=r;if(i.top!==l||i.left!==c||i.right!==v||i.bottom!==g){const T={target:o,boundingClientRect:r};this.entries.set(o,T),s.push(T)}})}),n(s)});this._tick=requestAnimationFrame(async()=>{const n=await t;n.length&&this._callback(n,this),this._runCallback()})};_new=t=>new Promise(n=>{new IntersectionObserver(([s],o)=>{o.disconnect(),n(s)}).observe(t)});getEntry=t=>this.entries.get(t);disconnect=()=>{cancelAnimationFrame(this._tick),this.entries.clear(),this._tick=0}}const Ht=["dropdown","dropup","dropstart","dropend"],ms="Dropdown",vs="dropdown-menu",bs=e=>{const t=M(e,"A");return e.tagName==="A"&&Yt(e,"href")&&tt(e,"href")?.slice(-1)==="#"||t&&Yt(t,"href")&&tt(t,"href")?.slice(-1)==="#"},[Z,Je,tn,en]=Ht,_i=`[${nt}="${Z}"]`,ne=e=>W(e,ms),Ii=e=>new rn(e),Oi=`${vs}-end`,ws=[Z,Je],Es=[tn,en],$s=["A","BUTTON"],Ni={offset:5,display:"dynamic"},nn=E(`show.bs.${Z}`),Ts=E(`shown.bs.${Z}`),sn=E(`hide.bs.${Z}`),ys=E(`hidden.bs.${Z}`),Cs=E(`updated.bs.${Z}`),Ss=e=>{const{element:t,menu:n,parentElement:s,options:o}=e,{offset:i}=o;if(N(n,"position")==="static")return;const r=gt(t),a=h(n,Oi);["margin","top","bottom","left","right"].forEach(B=>{const Ot={};Ot[B]="",P(n,Ot)});let l=Ht.find(B=>h(s,B))||Z;const g={dropdown:[i,0,0],dropup:[0,0,i],dropstart:r?[-1,0,0,i]:[-1,i,0],dropend:r?[-1,i,0]:[-1,0,0,i]},v={dropdown:{top:"100%"},dropup:{top:"auto",bottom:"100%"},dropstart:r?{left:"100%",right:"auto"}:{left:"auto",right:"100%"},dropend:r?{left:"auto",right:"100%"}:{left:"100%",right:"auto"},menuStart:r?{right:"0",left:"auto"}:{right:"auto",left:"0"},menuEnd:r?{right:"auto",left:"0"}:{right:"0",left:"auto"}},{offsetWidth:T,offsetHeight:V}=n,{clientWidth:st,clientHeight:wt}=ut(t),{left:m,top:q,width:_t,height:It}=Rt(t),C=m-T-i<0,Et=m+T+_t+i>=st,dt=q+V+i>=wt,J=q+V+It+i>=wt,zt=q-V-i<0,$=(!r&&a||r&&!a)&&m+_t-T<0,z=(r&&a||!r&&!a)&&m+T>=st;if(Es.includes(l)&&C&&Et&&(l=Z),l===tn&&(r?Et:C)&&(l=en),l===en&&(r?C:Et)&&(l=tn),l===Je&&zt&&!J&&(l=Z),l===Z&&J&&!zt&&(l=Je),Es.includes(l)&&dt&&ot(v[l],{top:"auto",bottom:0}),ws.includes(l)&&($||z)){let B={left:"auto",right:"auto"};!$&&z&&!r&&(B={left:"auto",right:0}),$&&!z&&r&&(B={left:0,right:"auto"}),B&&ot(v[l],B)}const Kt=g[l];P(n,{...v[l],margin:`${Kt.map(B=>B&&`${B}px`).join(" ")}`}),ws.includes(l)&&a&&a&&P(n,v[!r&&$||r&&z?"menuStart":"menuEnd"]),b(s,Cs)},Mi=e=>Array.from(e.children).map(t=>{if(t&&$s.includes(t.tagName))return t;const{firstElementChild:n}=t;return n&&$s.includes(n.tagName)?n:null}).filter(t=>t),Ps=e=>{const{element:t,options:n,menu:s}=e,o=e.open?_:I,i=w(t);o(i,k,xs),o(i,Le,xs),o(i,pe,Ri),o(i,Ao,Wi),n.display==="dynamic"&&(e.open?e._observer.observe(s):e._observer.disconnect())},on=e=>{const t=[...Ht,"btn-group","input-group"].map(n=>rt(`${n} ${u}`,w(e))).find(n=>n.length);if(t&&t.length)return[...t[0].children].find(n=>Ht.some(s=>s===tt(n,nt)))},xs=e=>{const{target:t,type:n}=e;if(!D(t))return;const s=on(t),o=s&&ne(s);if(!o)return;const{parentElement:i,menu:r}=o,a=i&&i.contains(t)&&(t.tagName==="form"||M(t,"form")!==null);[k,On].includes(n)&&bs(t)&&e.preventDefault(),!a&&n!==Le&&t!==s&&t!==r&&o.hide()};function Bi(e){const t=ne(this);at(this)||t&&(e.stopPropagation(),t.toggle(),bs(this)&&e.preventDefault())}const Ri=e=>{[Ne,Me].includes(e.code)&&e.preventDefault()};function Wi(e){const{code:t}=e,n=on(this);if(!n)return;const s=ne(n),{activeElement:o}=w(n);if(!s||!o)return;const{menu:i,open:r}=s,a=Mi(i);if(a&&a.length&&[Ne,Me].includes(t)){let c=a.indexOf(o);o===n?c=0:t===Me?c=c>1?c-1:0:t===Ne&&(c=cSs(this)),this._toggleEventListeners(!0))}get name(){return ms}get defaults(){return Ni}toggle(){this.open?this.hide():this.show()}show(){const{element:t,open:n,menu:s,parentElement:o}=this;if(n)return;const i=on(t),r=i&&ne(i);r&&r.hide(),[nn,Ts,Cs].forEach(a=>{a.relatedTarget=t}),b(o,nn),!nn.defaultPrevented&&(d(s,u),d(o,u),L(t,ue,"true"),Ss(this),this.open=!n,it(t),Ps(this),b(o,Ts))}hide(){const{element:t,open:n,menu:s,parentElement:o}=this;n&&([sn,ys].forEach(i=>{i.relatedTarget=t}),b(o,sn),!sn.defaultPrevented&&(p(s,u),p(o,u),L(t,ue,"false"),this.open=!n,Ps(this),b(o,ys)))}_toggleEventListeners=t=>{(t?_:I)(this.element,k,Bi)};dispose(){this.open&&this.hide(),this._toggleEventListeners(),super.dispose()}}const K="modal",an="Modal",cn="Offcanvas",Fi="fixed-top",ji="fixed-bottom",Ds="sticky-top",Hs="position-sticky",As=e=>[...rt(Fi,e),...rt(ji,e),...rt(Ds,e),...rt(Hs,e),...rt("is-fixed",e)],zi=e=>{const t=Pt(e);P(t,{paddingRight:"",overflow:""});const n=As(t);n.length&&n.forEach(s=>{P(s,{paddingRight:"",marginRight:""})})},ks=e=>{const{clientWidth:t}=ut(e),{innerWidth:n}=Vn(e);return Math.abs(n-t)},Ls=(e,t)=>{const n=Pt(e),s=parseInt(N(n,"paddingRight"),10),i=N(n,"overflow")==="hidden"&&s?0:ks(e),r=As(n);t&&(P(n,{overflow:"hidden",paddingRight:`${s+i}px`}),r.length&&r.forEach(a=>{const c=N(a,"paddingRight");if(a.style.paddingRight=`${parseInt(c,10)+i}px`,[Ds,Hs].some(l=>h(a,l))){const l=N(a,"marginRight");a.style.marginRight=`${parseInt(l,10)-i}px`}}))},U="offcanvas",vt=ht({tagName:"div",className:"popup-container"}),_s=(e,t)=>{const n=x(t)&&t.nodeName==="BODY",s=x(t)&&!n?t:vt,o=n?t:Pt(e);x(e)&&(s===vt&&o.append(vt),s.append(e))},Is=(e,t)=>{const n=x(t)&&t.nodeName==="BODY",s=x(t)&&!n?t:vt;x(e)&&(e.remove(),s===vt&&!vt.children.length&&vt.remove())},ln=(e,t)=>{const n=x(t)&&t.nodeName!=="BODY"?t:vt;return x(e)&&n.contains(e)},Os="backdrop",Ns=`${K}-${Os}`,Ms=`${U}-${Os}`,Bs=`.${K}.${u}`,dn=`.${U}.${u}`,A=ht("div"),At=e=>H(`${Bs},${dn}`,w(e)),hn=e=>{const t=e?Ns:Ms;[Ns,Ms].forEach(n=>{p(A,n)}),d(A,t)},Rs=(e,t,n)=>{hn(n),_s(A,Pt(e)),t&&d(A,O)},Ws=()=>{h(A,u)||(d(A,u),St(A))},Ce=()=>{p(A,u)},Fs=e=>{At(e)||(p(A,O),Is(A,Pt(e)),zi(e))},js=e=>D(e)&&N(e,"visibility")!=="hidden"&&e.offsetParent!==null,Ki=`.${K}`,Vi=`[${nt}="${K}"]`,qi=`[${Ee}="${K}"]`,zs=`${K}-static`,Xi={backdrop:!0,keyboard:!0},se=e=>W(e,an),Yi=e=>new un(e),Se=E(`show.bs.${K}`),Ks=E(`shown.bs.${K}`),fn=E(`hide.bs.${K}`),Vs=E(`hidden.bs.${K}`),qs=e=>{const{element:t}=e,n=ks(t),{clientHeight:s,scrollHeight:o}=ut(t),{clientHeight:i,scrollHeight:r}=t,a=i!==r;if(!a&&n){const l={[gt(t)?"paddingLeft":"paddingRight"]:`${n}px`};P(t,l)}Ls(t,a||s!==o)},Xs=(e,t)=>{const n=t?_:I,{element:s}=e;n(s,k,Qi),n(w(s),pe,Gi),t?e._observer.observe(s):e._observer.disconnect()},Ys=e=>{const{triggers:t,element:n,relatedTarget:s}=e;Fs(n),P(n,{paddingRight:"",display:""}),Xs(e);const o=Se.relatedTarget||t.find(js);o&&it(o),Vs.relatedTarget=s||void 0,b(n,Vs),we(n)},Us=e=>{const{element:t,relatedTarget:n}=e;it(t),Xs(e,!0),Ks.relatedTarget=n||void 0,b(t,Ks),we(t)},Gs=e=>{const{element:t,hasFade:n}=e;P(t,{display:"block"}),qs(e),At(t)||P(Pt(t),{overflow:"hidden"}),d(t,u),yt(t,Xt),L(t,ge,"true"),n?S(t,()=>Us(e)):Us(e)},Qs=e=>{const{element:t,options:n,hasFade:s}=e;n.backdrop&&s&&h(A,u)&&!At(t)?(Ce(),S(A,()=>Ys(e))):Ys(e)};function Ui(e){const t=F(this),n=t&&se(t);at(this)||n&&(this.tagName==="A"&&e.preventDefault(),n.relatedTarget=this,n.toggle())}const Gi=({code:e,target:t})=>{const n=H(Bs,w(t)),s=n&&se(n);if(!s)return;const{options:o}=s;o.keyboard&&e===Be&&h(n,u)&&(s.relatedTarget=null,s.hide())},Qi=e=>{const{currentTarget:t}=e,n=t&&se(t);if(!n||!t||f.get(t))return;const{options:s,isStatic:o,modalDialog:i}=n,{backdrop:r}=s,{target:a}=e,c=w(t)?.getSelection()?.toString().length,l=i.contains(a),g=a&&M(a,qi);o&&!l?f.set(t,()=>{d(t,zs),S(i,()=>Zi(n))},17):(g||!c&&!o&&!l&&r)&&(n.relatedTarget=g||null,n.hide(),e.preventDefault())},Zi=e=>{const{element:t,modalDialog:n}=e,s=(Qt(n)||0)+17;p(t,zs),f.set(t,()=>f.clear(t),s)};class un extends et{static selector=Ki;static init=Yi;static getInstance=se;constructor(t,n){super(t,n);const{element:s}=this,o=H(`.${K}-dialog`,s);o&&(this.modalDialog=o,this.triggers=[...Y(Vi,w(s))].filter(i=>F(i)===s),this.isStatic=this.options.backdrop==="static",this.hasFade=h(s,O),this.relatedTarget=null,this._observer=new ResizeObserver(()=>this.update()),this._toggleEventListeners(!0))}get name(){return an}get defaults(){return Xi}toggle(){h(this.element,u)?this.hide():this.show()}show(){const{element:t,options:n,hasFade:s,relatedTarget:o}=this,{backdrop:i}=n;let r=0;if(h(t,u)||(Se.relatedTarget=o||void 0,b(t,Se),Se.defaultPrevented))return;const a=At(t);if(a&&a!==t){const c=se(a)||W(a,cn);c&&c.hide()}i?(ln(A)?hn(!0):Rs(t,s,!0),r=Qt(A),Ws(),setTimeout(()=>Gs(this),r)):(Gs(this),a&&h(A,u)&&Ce())}hide(){const{element:t,hasFade:n,relatedTarget:s}=this;h(t,u)&&(fn.relatedTarget=s||void 0,b(t,fn),!fn.defaultPrevented&&(p(t,u),L(t,Xt,"true"),yt(t,ge),n?S(t,()=>Qs(this)):Qs(this)))}update=()=>{h(this.element,u)&&qs(this)};_toggleEventListeners=t=>{const n=t?_:I,{triggers:s}=this;s.length&&s.forEach(o=>{n(o,k,Ui)})};dispose(){const t={...this},{modalDialog:n,hasFade:s}=t,o=()=>setTimeout(()=>super.dispose(),17);this.hide(),this._toggleEventListeners(),s?S(n,o):o()}}const Ji=`.${U}`,Zs=`[${nt}="${U}"]`,tr=`[${Ee}="${U}"]`,Pe=`${U}-toggling`,er={backdrop:!0,keyboard:!0,scroll:!1},oe=e=>W(e,cn),nr=e=>new pn(e),xe=E(`show.bs.${U}`),Js=E(`shown.bs.${U}`),gn=E(`hide.bs.${U}`),to=E(`hidden.bs.${U}`),sr=e=>{const{element:t}=e,{clientHeight:n,scrollHeight:s}=ut(t);Ls(t,n!==s)},eo=(e,t)=>{const n=t?_:I,s=w(e.element);n(s,pe,ar),n(s,k,rr)},no=e=>{const{element:t,options:n}=e;n.scroll||(sr(e),P(Pt(t),{overflow:"hidden"})),d(t,Pe),d(t,u),P(t,{visibility:"visible"}),S(t,()=>cr(e))},or=e=>{const{element:t,options:n}=e,s=At(t);t.blur(),!s&&n.backdrop&&h(A,u)&&Ce(),S(t,()=>lr(e))};function ir(e){const t=F(this),n=t&&oe(t);at(this)||n&&(n.relatedTarget=this,n.toggle(),this.tagName==="A"&&e.preventDefault())}const rr=e=>{const{target:t}=e,n=H(dn,w(t));if(!n)return;const s=H(tr,n),o=oe(n);if(!o)return;const{options:i,triggers:r}=o,{backdrop:a}=i,c=M(t,Zs),l=w(n).getSelection();A.contains(t)&&a==="static"||(!(l&&l.toString().length)&&(!n.contains(t)&&a&&(!c||r.includes(t))||s&&s.contains(t))&&(o.relatedTarget=s&&s.contains(t)?s:void 0,o.hide()),c&&c.tagName==="A"&&e.preventDefault())},ar=({code:e,target:t})=>{const n=H(dn,w(t)),s=n&&oe(n);s&&s.options.keyboard&&e===Be&&(s.relatedTarget=void 0,s.hide())},cr=e=>{const{element:t}=e;p(t,Pe),yt(t,Xt),L(t,ge,"true"),L(t,"role","dialog"),b(t,Js),eo(e,!0),it(t),we(t)},lr=e=>{const{element:t,triggers:n}=e;L(t,Xt,"true"),yt(t,ge),yt(t,"role"),P(t,{visibility:""});const s=xe.relatedTarget||n.find(js);s&&it(s),Fs(t),b(t,to),p(t,Pe),we(t),At(t)||eo(e)};class pn extends et{static selector=Ji;static init=nr;static getInstance=oe;constructor(t,n){super(t,n);const{element:s}=this;this.triggers=[...Y(Zs,w(s))].filter(o=>F(o)===s),this.relatedTarget=void 0,this._toggleEventListeners(!0)}get name(){return cn}get defaults(){return er}toggle(){h(this.element,u)?this.hide():this.show()}show(){const{element:t,options:n,relatedTarget:s}=this;let o=0;if(h(t,u)||(xe.relatedTarget=s||void 0,Js.relatedTarget=s||void 0,b(t,xe),xe.defaultPrevented))return;const i=At(t);if(i&&i!==t){const r=oe(i)||W(i,an);r&&r.hide()}n.backdrop?(ln(A)?hn():Rs(t,!0),o=Qt(A),Ws(),setTimeout(()=>no(this),o)):(no(this),i&&h(A,u)&&Ce())}hide(){const{element:t,relatedTarget:n}=this;h(t,u)&&(gn.relatedTarget=n||void 0,to.relatedTarget=n||void 0,b(t,gn),!gn.defaultPrevented&&(d(t,Pe),p(t,u),or(this)))}_toggleEventListeners=t=>{const n=t?_:I;this.triggers.forEach(s=>{n(s,k,ir)})};dispose(){const{element:t}=this,n=h(t,u),s=()=>setTimeout(()=>super.dispose(),1);this.hide(),this._toggleEventListeners(),n?S(t,s):s()}}const kt="popover",mn="Popover",lt="tooltip",so=e=>{const t=e===lt,n=t?`${e}-inner`:`${e}-body`,s=t?"":`

`,o=`
`,i=`
`;return`
${s+o+i}
`},oo={top:"top",bottom:"bottom",left:"start",right:"end"},vn=e=>{requestAnimationFrame(()=>{const t=/\b(top|bottom|start|end)+/,{element:n,tooltip:s,container:o,offsetParent:i,options:r,arrow:a}=e;if(!s)return;const c=gt(n),{x:l,y:g}=Xo(i);P(s,{top:"",left:"",right:"",bottom:""});const{offsetWidth:v,offsetHeight:T}=s,{clientWidth:V,clientHeight:st,offsetWidth:wt}=ut(n);let{placement:m}=r;const{clientWidth:q,offsetWidth:_t}=o,C=N(o,"position")==="fixed",Et=Math.abs(C?q-_t:V-wt),dt=c&&C?Et:0,J=V-(c?0:Et)-1,zt=e._observer.getEntry(n),{width:$,height:z,left:Kt,right:B,top:Ot}=zt?.boundingClientRect||Rt(n,!0),{x:De,y:Vt}=Qo(n,i,{x:l,y:g});P(a,{top:"",left:"",right:"",bottom:""});let Nt=0,ce="",$t=0,Dn="",qt="",He="",Hn="";const Mt=a.offsetWidth||0,Tt=a.offsetHeight||0,An=Mt/2;let le=Ot-T-Tt<0,de=Ot+T+z+Tt>=st,he=Kt-v-Mt=J;const Ae=["left","right"],kn=["top","bottom"];le=Ae.includes(m)?Ot+z/2-T/2-Tt<0:le,de=Ae.includes(m)?Ot+T/2+z/2+Tt>=st:de,he=kn.includes(m)?Kt+$/2-v/2=J:fe,m=Ae.includes(m)&&he&&fe?"top":m,m=m==="top"&&le?"bottom":m,m=m==="bottom"&&de?"top":m,m=m==="left"&&he?"right":m,m=m==="right"&&fe?"left":m,s.className.includes(m)||(s.className=s.className.replace(t,oo[m])),Ae.includes(m)?(m==="left"?$t=De-v-Mt:$t=De+$+Mt,le&&de?(Nt=0,ce=0,qt=Vt+z/2-Tt/2):le?(Nt=Vt,ce="",qt=z/2-Mt):de?(Nt=Vt-T+z,ce="",qt=T-z/2-Mt):(Nt=Vt-T/2+z/2,qt=T/2-Tt/2)):kn.includes(m)&&(m==="top"?Nt=Vt-T-Tt:Nt=Vt+z+Tt,he?($t=0,He=De+$/2-An):fe?($t="auto",Dn=0,Hn=$/2+J-B-An):($t=De-v/2+$/2,He=v/2-An)),P(s,{top:`${Nt}px`,bottom:ce===""?"":`${ce}px`,left:$t==="auto"?$t:`${$t}px`,right:Dn!==""?`${Dn}px`:""}),D(a)&&(qt!==""&&(a.style.top=`${qt}px`),He!==""?a.style.left=`${He}px`:Hn!==""&&(a.style.right=`${Hn}px`));const Nr=E(`updated.bs.${Jt(e.name)}`);b(n,Nr)})},bn={template:so(lt),title:"",customClass:"",trigger:"hover focus",placement:"top",sanitizeFn:void 0,animation:!0,delay:200,container:document.body,content:"",dismissible:!1,btnClose:""},io="data-original-title",Lt="Tooltip",bt=(e,t,n)=>{if(Gt(t)&&t.length){let s=t.trim();Jo(n)&&(s=n(s));const i=new DOMParser().parseFromString(s,"text/html");e.append(...i.body.childNodes)}else D(t)?e.append(t):(ti(t)||Zo(t)&&t.every(x))&&e.append(...t)},dr=e=>{const t=e.name===Lt,{id:n,element:s,options:o}=e,{title:i,placement:r,template:a,animation:c,customClass:l,sanitizeFn:g,dismissible:v,content:T,btnClose:V}=o,st=t?lt:kt,wt={...oo};let m=[],q=[];gt(s)&&(wt.left="end",wt.right="start");const _t=`bs-${st}-${wt[r]}`;let It;if(D(a))It=a;else{const $=ht("div");bt($,a,g),It=$.firstChild}if(!D(It))return;e.tooltip=It.cloneNode(!0);const{tooltip:C}=e;L(C,"id",n),L(C,"role",lt);const Et=t?`${lt}-inner`:`${kt}-body`,dt=t?null:H(`.${kt}-header`,C),J=H(`.${Et}`,C);e.arrow=H(`.${st}-arrow`,C);const{arrow:zt}=e;if(D(i))m=[i.cloneNode(!0)];else{const $=ht("div");bt($,i,g),m=[...$.childNodes]}if(D(T))q=[T.cloneNode(!0)];else{const $=ht("div");bt($,T,g),q=[...$.childNodes]}if(v)if(i)if(D(V))m=[...m,V.cloneNode(!0)];else{const $=ht("div");bt($,V,g),m=[...m,$.firstChild]}else if(dt&&dt.remove(),D(V))q=[...q,V.cloneNode(!0)];else{const $=ht("div");bt($,V,g),q=[...q,$.firstChild]}t?i&&J&&bt(J,i,g):(i&&dt&&bt(dt,m,g),T&&J&&bt(J,q,g),e.btn=H(".btn-close",C)||void 0),d(C,"position-absolute"),d(zt,"position-absolute"),h(C,st)||d(C,st),c&&!h(C,O)&&d(C,O),l&&!h(C,l)&&d(C,l),h(C,_t)||d(C,_t)},hr=e=>{const t=["HTML","BODY"],n=[];let{parentNode:s}=e;for(;s&&!t.includes(s.nodeName);)s=Yo(s),Kn(s)||Uo(s)||n.push(s);return n.find((o,i)=>(N(o,"position")!=="relative"||N(o,"position")==="relative"&&o.offsetHeight!==o.scrollHeight)&&n.slice(i+1).every(r=>N(r,"position")==="static")?o:null)||w(e).body},fr=`[${nt}="${lt}"],[data-tip="${lt}"]`,ro="title";let ao=e=>W(e,Lt);const ur=e=>new go(e),gr=e=>{const{element:t,tooltip:n,container:s}=e;yt(t,Ln),Is(n,s)},ie=e=>{const{tooltip:t,container:n}=e;return t&&ln(t,n)},pr=(e,t)=>{const{element:n}=e;e._toggleEventListeners(),Yt(n,io)&&e.name===Lt&&uo(e),t&&t()},co=(e,t)=>{const n=t?_:I,{element:s}=e;n(w(s),Oe,e.handleTouch,Zt)},lo=e=>{const{element:t}=e,n=E(`shown.bs.${Jt(e.name)}`);co(e,!0),b(t,n),f.clear(t,"in")},ho=e=>{const{element:t}=e,n=E(`hidden.bs.${Jt(e.name)}`);co(e),gr(e),b(t,n),f.clear(t,"out")},fo=(e,t)=>{const n=t?_:I,{element:s,tooltip:o}=e,i=M(s,`.${K}`),r=M(s,`.${U}`);t?[s,o].forEach(a=>e._observer.observe(a)):e._observer.disconnect(),i&&n(i,`hide.bs.${K}`,e.handleHide),r&&n(r,`hide.bs.${U}`,e.handleHide)},uo=(e,t)=>{const n=[io,ro],{element:s}=e;L(s,n[t?0:1],t||tt(s,n[0])||""),yt(s,n[t?1:0])};class go extends et{static selector=fr;static init=ur;static getInstance=ao;static styleTip=vn;constructor(t,n){super(t,n);const{element:s}=this,o=this.name===Lt,i=o?lt:kt,r=o?Lt:mn;ao=g=>W(g,r),this.enabled=!0,this.id=`${i}-${Un(s,i)}`;const{options:a}=this;if(!a.title&&o||!o&&!a.content)return;ot(bn,{titleAttr:""}),Yt(s,ro)&&o&&typeof a.title=="string"&&uo(this,a.title);const c=hr(s),l=["sticky","fixed","relative"].some(g=>N(c,"position")===g)?c:Vn(s);this.container=c,this.offsetParent=l,dr(this),this.tooltip&&(this._observer=new ps(()=>this.update()),this._toggleEventListeners(!0))}get name(){return Lt}get defaults(){return bn}handleFocus=()=>it(this.element);handleShow=()=>this.show();show(){const{options:t,tooltip:n,element:s,container:o,id:i}=this,{animation:r}=t,a=f.get(s,"out");f.clear(s,"out"),n&&!a&&!ie(this)&&f.set(s,()=>{const c=E(`show.bs.${Jt(this.name)}`);b(s,c),c.defaultPrevented||(_s(n,o),L(s,Ln,`#${i}`),this.update(),fo(this,!0),h(n,u)||d(n,u),r?S(n,()=>lo(this)):lo(this))},17,"in")}handleHide=()=>this.hide();hide(){const{options:t,tooltip:n,element:s}=this,{animation:o,delay:i}=t;f.clear(s,"in"),n&&ie(this)&&f.set(s,()=>{const r=E(`hide.bs.${Jt(this.name)}`);b(s,r),r.defaultPrevented||(this.update(),p(n,u),fo(this),o?S(n,()=>ho(this)):ho(this))},i+17,"out")}update=()=>{vn(this)};toggle=()=>{const{tooltip:t}=this;t&&!ie(this)?this.show():this.hide()};enable(){const{enabled:t}=this;t||(this._toggleEventListeners(!0),this.enabled=!t)}disable(){const{tooltip:t,enabled:n}=this;n&&(t&&ie(this)&&this.hide(),this._toggleEventListeners(),this.enabled=!n)}toggleEnabled(){this.enabled?this.disable():this.enable()}handleTouch=({target:t})=>{const{tooltip:n,element:s}=this;n&&n.contains(t)||t===s||t&&s.contains(t)||this.hide()};_toggleEventListeners=t=>{const n=t?_:I,{element:s,options:o,btn:i}=this,{trigger:r}=o,c=!!(this.name!==Lt&&o.dismissible);r.includes("manual")||(this.enabled=!!t,r.split(" ").forEach(g=>{g===ko?(n(s,On,this.handleShow),n(s,me,this.handleShow),c||(n(s,Ie,this.handleHide),n(w(s),Oe,this.handleTouch,Zt))):g===k?n(s,g,c?this.handleShow:this.toggle):g===Le&&(n(s,_e,this.handleShow),c||n(s,In,this.handleHide),Ro()&&n(s,k,this.handleFocus)),c&&i&&n(i,k,this.handleHide)}))};dispose(){const{tooltip:t,options:n}=this,s={...this,name:this.name},o=()=>setTimeout(()=>pr(s,()=>super.dispose()),17);n.animation&&ie(s)?(this.options.delay=0,this.hide(),S(t,o)):o()}}const mr=`[${nt}="${kt}"],[data-tip="${kt}"]`,vr=ot({},bn,{template:so(kt),content:"",dismissible:!1,btnClose:''}),br=e=>W(e,mn),wr=e=>new wn(e);class wn extends go{static selector=mr;static init=wr;static getInstance=br;static styleTip=vn;constructor(t,n){super(t,n)}get name(){return mn}get defaults(){return vr}show=()=>{super.show();const{options:t,btn:n}=this;t.dismissible&&n&&setTimeout(()=>it(n),17)}}const re="tab",po="Tab",En=`[${nt}="${re}"]`,mo=e=>W(e,po),Er=e=>new yn(e),$n=E(`show.bs.${re}`),vo=E(`shown.bs.${re}`),Tn=E(`hide.bs.${re}`),bo=E(`hidden.bs.${re}`),ae=new Map,wo=e=>{const{tabContent:t,nav:n}=e;t&&h(t,Dt)&&(t.style.height="",p(t,Dt)),n&&f.clear(n)},Eo=e=>{const{element:t,tabContent:n,content:s,nav:o}=e,{tab:i}=D(o)&&ae.get(o)||{tab:null};if(n&&s&&h(s,O)){const{currentHeight:r,nextHeight:a}=ae.get(t)||{currentHeight:0,nextHeight:0};r!==a?setTimeout(()=>{n.style.height=`${a}px`,St(n),S(n,()=>wo(e))},50):wo(e)}else o&&f.clear(o);vo.relatedTarget=i,b(t,vo)},$o=e=>{const{element:t,content:n,tabContent:s,nav:o}=e,{tab:i,content:r}=o&&ae.get(o)||{tab:null,content:null};let a=0;if(s&&n&&h(n,O)&&([r,n].forEach(c=>{c&&d(c,"overflow-hidden")}),a=r?r.scrollHeight:0),$n.relatedTarget=i,bo.relatedTarget=t,b(t,$n),!$n.defaultPrevented){if(n&&d(n,y),r&&p(r,y),s&&n&&h(n,O)){const c=n.scrollHeight;ae.set(t,{currentHeight:a,nextHeight:c,tab:null,content:null}),d(s,Dt),s.style.height=`${a}px`,St(s),[r,n].forEach(l=>{l&&p(l,"overflow-hidden")})}n&&n&&h(n,O)?setTimeout(()=>{d(n,u),S(n,()=>{Eo(e)})},1):(n&&d(n,u),Eo(e)),i&&b(i,bo)}},To=e=>{const{nav:t}=e;if(!D(t))return{tab:null,content:null};const n=rt(y,t);let s=null;n.length===1&&!Ht.some(i=>h(n[0].parentElement,i))?[s]=n:n.length>1&&(s=n[n.length-1]);const o=D(s)?F(s):null;return{tab:s,content:o}},yo=e=>{if(!D(e))return null;const t=M(e,`.${Ht.join(",.")}`);return t?H(`.${Ht[0]}-toggle`,t):null},$r=e=>{const t=M(e.target,En),n=t&&mo(t);n&&(e.preventDefault(),n.show())};class yn extends et{static selector=En;static init=Er;static getInstance=mo;constructor(t){super(t);const{element:n}=this,s=F(n);if(!s)return;const o=M(n,".nav"),i=M(s,".tab-content");this.nav=o,this.content=s,this.tabContent=i,this.dropdown=yo(n);const{tab:r}=To(this);if(o&&!r){const a=H(En,o),c=a&&F(a);c&&(d(a,y),d(c,u),d(c,y),L(n,ke,"true"))}this._toggleEventListeners(!0)}get name(){return po}show(){const{element:t,content:n,nav:s,dropdown:o}=this;if(s&&f.get(s)||h(t,y))return;const{tab:i,content:r}=To(this);if(s&&i&&ae.set(s,{tab:i,content:r,currentHeight:0,nextHeight:0}),Tn.relatedTarget=t,!D(i)||(b(i,Tn),Tn.defaultPrevented))return;d(t,y),L(t,ke,"true");const a=D(i)&&yo(i);if(a&&h(a,y)&&p(a,y),s){const c=()=>{i&&(p(i,y),L(i,ke,"false")),o&&!h(o,y)&&d(o,y)};r&&(h(r,O)||n&&h(n,O))?f.set(s,c,1):c()}r&&(p(r,u),h(r,O)?S(r,()=>$o(this)):$o(this))}_toggleEventListeners=t=>{(t?_:I)(this.element,k,$r)};dispose(){this._toggleEventListeners(),super.dispose()}}const G="toast",Co="Toast",Tr=`.${G}`,yr=`[${Ee}="${G}"]`,Cr=`[${nt}="${G}"]`,jt="showing",So="hide",Sr={animation:!0,autohide:!0,delay:5e3},Cn=e=>W(e,Co),Pr=e=>new Sn(e),Po=E(`show.bs.${G}`),xr=E(`shown.bs.${G}`),xo=E(`hide.bs.${G}`),Dr=E(`hidden.bs.${G}`),Do=e=>{const{element:t,options:n}=e;p(t,jt),f.clear(t,jt),b(t,xr),n.autohide&&f.set(t,()=>e.hide(),n.delay,G)},Ho=e=>{const{element:t}=e;p(t,jt),p(t,u),d(t,So),f.clear(t,G),b(t,Dr)},Hr=e=>{const{element:t,options:n}=e;d(t,jt),n.animation?(St(t),S(t,()=>Ho(e))):Ho(e)},Ar=e=>{const{element:t,options:n}=e;f.set(t,()=>{p(t,So),St(t),d(t,u),d(t,jt),n.animation?S(t,()=>Do(e)):Do(e)},17,jt)};function kr(e){const t=F(this),n=t&&Cn(t);at(this)||n&&(this.tagName==="A"&&e.preventDefault(),n.relatedTarget=this,n.show())}const Lr=e=>{const t=e.target,n=Cn(t),{type:s,relatedTarget:o}=e;!n||t===o||t.contains(o)||([me,_e].includes(s)?f.clear(t,G):f.set(t,()=>n.hide(),n.options.delay,G))};class Sn extends et{static selector=Tr;static init=Pr;static getInstance=Cn;constructor(t,n){super(t,n);const{element:s,options:o}=this;o.animation&&!h(s,O)?d(s,O):!o.animation&&h(s,O)&&p(s,O),this.dismiss=H(yr,s),this.triggers=[...Y(Cr,w(s))].filter(i=>F(i)===s),this._toggleEventListeners(!0)}get name(){return Co}get defaults(){return Sr}get isShown(){return h(this.element,u)}show=()=>{const{element:t,isShown:n}=this;!t||n||(b(t,Po),Po.defaultPrevented||Ar(this))};hide=()=>{const{element:t,isShown:n}=this;!t||!n||(b(t,xo),xo.defaultPrevented||Hr(this))};_toggleEventListeners=t=>{const n=t?_:I,{element:s,triggers:o,dismiss:i,options:r,hide:a}=this;i&&n(i,k,a),r.autohide&&[_e,In,me,Ie].forEach(c=>n(s,c,Lr)),o.length&&o.forEach(c=>{n(c,k,kr)})};dispose(){const{element:t,isShown:n}=this;this._toggleEventListeners(),f.clear(t,G),n&&p(t,u),super.dispose()}}const Pn=new Map;[je,ze,Qe,Ze,rn,un,pn,wn,yn,Sn].forEach(e=>Pn.set(e.prototype.name,e));const _r=(e,t)=>{[...t].forEach(n=>e(n))},Ir=(e,t)=>{const n=Ct.getAllFor(e);n&&[...n].forEach(([s,o])=>{t.contains(s)&&o.dispose()})},xn=e=>{const t=e&&e.nodeName?e:document,n=[...ei("*",t)];Pn.forEach(s=>{const{init:o,selector:i}=s;_r(o,n.filter(r=>qn(r,i)))})},Or=e=>{const t=e&&e.nodeName?e:document;Pn.forEach(n=>{Ir(n.prototype.name,t)})};return document.body?xn():_(document,"DOMContentLoaded",()=>xn(),{once:!0}),R.Alert=je,R.Button=ze,R.Carousel=Qe,R.Collapse=Ze,R.Dropdown=rn,R.Modal=un,R.Offcanvas=pn,R.Popover=wn,R.Tab=yn,R.Toast=Sn,R.initCallback=xn,R.removeDataAPI=Or,Object.defineProperty(R,Symbol.toStringTag,{value:"Module"}),R}({}); diff --git a/dist/bootstrap-native/mympd-config.ts b/dist/bootstrap-native/mympd-config.ts index eb1af289a..ee327e137 100644 --- a/dist/bootstrap-native/mympd-config.ts +++ b/dist/bootstrap-native/mympd-config.ts @@ -1,5 +1,3 @@ -import * as Listener from '@thednp/event-listener'; - import Alert from './components/alert'; import Button from './components/button'; import Carousel from './components/carousel'; @@ -20,13 +18,13 @@ export { Carousel, Collapse, Dropdown, + initCallback, Modal, Offcanvas, Popover, + removeDataAPI, Tab, Toast, - initCallback, - removeDataAPI, - // Version, - Listener, -}; \ No newline at end of file +}; + +export {}; diff --git a/dist/bootstrap-native/mympd-init.ts b/dist/bootstrap-native/mympd-init.ts index f9451e95a..dfa8db224 100644 --- a/dist/bootstrap-native/mympd-init.ts +++ b/dist/bootstrap-native/mympd-init.ts @@ -1,19 +1,45 @@ -import { Data, ObjectKeys, ObjectValues, getElementsByTagName, matches } from '@thednp/shorty'; +import { Data, getElementsByTagName, matches } from "@thednp/shorty"; -import { addListener } from '@thednp/event-listener'; +import { addListener } from "@thednp/event-listener"; -import Alert from '../components/alert'; -import Button from '../components/button'; -import Carousel from '../components/carousel'; -import Collapse from '../components/collapse'; -import Dropdown from '../components/dropdown'; -import Modal from '../components/modal'; -import Offcanvas from '../components/offcanvas'; -import Popover from '../components/popover'; -import Tab from '../components/tab'; -import Toast from '../components/toast'; +import Alert from "../components/alert"; +import Button from "../components/button"; +import Carousel from "../components/carousel"; +import Collapse from "../components/collapse"; +import Dropdown from "../components/dropdown"; +import Modal from "../components/modal"; +import Offcanvas from "../components/offcanvas"; +import Popover from "../components/popover"; +import Tab from "../components/tab"; +import Toast from "../components/toast"; -const componentsList = { +const componentsList = new Map< + string, + | typeof Alert + | typeof Button + | typeof Carousel + | typeof Collapse + | typeof Dropdown + | typeof Modal + | typeof Offcanvas + | typeof Popover + | typeof Tab + | typeof Toast +>(); + +type Component = + | Alert + | Button + | Carousel + | Collapse + | Dropdown + | Modal + | Offcanvas + | Popover + | Tab + | Toast; + +[ Alert, Button, Carousel, @@ -24,7 +50,7 @@ const componentsList = { Popover, Tab, Toast, -}; +].forEach((c) => componentsList.set(c.prototype.name, c)); /** * Initialize all matched `Element`s for one component. @@ -32,11 +58,11 @@ const componentsList = { * @param callback * @param collection */ -const initComponentDataAPI = ( - callback: (el: HTMLElement, ops?: Record) => T, - collection: HTMLCollectionOf | HTMLElement[], +const initComponentDataAPI = ( + callback: (el: Element) => Component, + collection: HTMLCollectionOf | Element[], ) => { - [...collection].forEach(x => callback(x)); + [...collection].forEach((x) => callback(x)); }; /** @@ -46,11 +72,13 @@ const initComponentDataAPI = ( * @param context parent `Node` */ const removeComponentDataAPI = (component: string, context: ParentNode) => { - const compData = Data.getAllFor(component) as Map; + const compData = Data.getAllFor(component) as Map; if (compData) { [...compData].forEach(([element, instance]) => { - if (context.contains(element)) (instance as T & { dispose: () => void }).dispose(); + if (context.contains(element)) { + (instance as T & { dispose: () => void }).dispose(); + } }); } }; @@ -62,13 +90,13 @@ const removeComponentDataAPI = (component: string, context: ParentNode) => { */ export const initCallback = (context?: ParentNode) => { const lookUp = context && context.nodeName ? context : document; - const elemCollection = [...getElementsByTagName('*', lookUp)]; + const elemCollection = [...getElementsByTagName("*", lookUp)]; - ObjectValues(componentsList).forEach(cs => { + componentsList.forEach((cs) => { const { init, selector } = cs; initComponentDataAPI( init, - elemCollection.filter(item => matches(item, selector)), + elemCollection.filter((item) => matches(item, selector)), ); }); }; @@ -81,13 +109,15 @@ export const initCallback = (context?: ParentNode) => { export const removeDataAPI = (context?: ParentNode) => { const lookUp = context && context.nodeName ? context : document; - ObjectKeys(componentsList).forEach(comp => { - removeComponentDataAPI(comp, lookUp); + componentsList.forEach((comp) => { + removeComponentDataAPI(comp.prototype.name, lookUp); }); }; -// bulk initialize all components +// Bulk initialize all components if (document.body) initCallback(); else { - addListener(document, 'DOMContentLoaded', () => initCallback(), { once: true }); + addListener(document, "DOMContentLoaded", () => initCallback(), { + once: true, + }); } diff --git a/dist/libmympdclient/include/mpd/capabilities.h b/dist/libmympdclient/include/mpd/capabilities.h index 3ec128239..262e04c7f 100644 --- a/dist/libmympdclient/include/mpd/capabilities.h +++ b/dist/libmympdclient/include/mpd/capabilities.h @@ -218,6 +218,31 @@ mpd_send_all_tag_types(struct mpd_connection *connection); bool mpd_run_all_tag_types(struct mpd_connection *connection); +/** + * Clear the list of tag types and re-enable one or more tags from + * the list of tag types for this client. These will no longer be + * hidden from responses to this client. + * + * @param connection the connection to MPD + * @param types an array of tag types to enable + * @param n the number of tag types in the array + * @return true on success, false on error + * + * @since libmpdclient 2.23, MPD 0.24 + */ +bool +mpd_send_reset_tag_types(struct mpd_connection *connection, + const enum mpd_tag_type *types, unsigned n); + +/** + * Shortcut for mpd_send_reset_tag_types() and mpd_response_finish(). + * + * @since libmpdclient 2.23, MPD 0.24 + */ +bool +mpd_run_reset_tag_types(struct mpd_connection *connection, + const enum mpd_tag_type *types, unsigned n); + /** * Requests a list of enabled protocol features. * Use mpd_recv_protocol_feature_pair() to obtain the list of diff --git a/dist/libmympdclient/include/mpd/meson.build b/dist/libmympdclient/include/mpd/meson.build index cce235925..4592a2249 100644 --- a/dist/libmympdclient/include/mpd/meson.build +++ b/dist/libmympdclient/include/mpd/meson.build @@ -19,6 +19,7 @@ install_headers( 'directory.h', 'entity.h', 'error.h', + 'feature.h', 'fingerprint.h', 'idle.h', 'list.h', diff --git a/dist/libmympdclient/src/capabilities.c b/dist/libmympdclient/src/capabilities.c index dd6270739..7a91e7275 100644 --- a/dist/libmympdclient/src/capabilities.c +++ b/dist/libmympdclient/src/capabilities.c @@ -135,6 +135,21 @@ mpd_run_all_tag_types(struct mpd_connection *connection) mpd_response_finish(connection); } +bool +mpd_send_reset_tag_types(struct mpd_connection *connection, + const enum mpd_tag_type *types, unsigned n) +{ + return mpd_send_tag_types_v(connection, "reset", types, n); +} + +bool +mpd_run_reset_tag_types(struct mpd_connection *connection, + const enum mpd_tag_type *types, unsigned n) +{ + return mpd_send_reset_tag_types(connection, types, n) && + mpd_response_finish(connection); +} + bool mpd_send_list_protocol_features(struct mpd_connection *connection) { diff --git a/dist/mongoose/mongoose.c b/dist/mongoose/mongoose.c index 38d39a7de..220137098 100644 --- a/dist/mongoose/mongoose.c +++ b/dist/mongoose/mongoose.c @@ -117,6308 +117,6761 @@ size_t mg_base64_decode(const char *src, size_t n, char *dst, size_t dl) { } #ifdef MG_ENABLE_LINES -#line 1 "src/device_ch32v307.c" +#line 1 "src/dns.c" #endif -#if MG_DEVICE == MG_DEVICE_CH32V307 -// RM: https://www.wch-ic.com/downloads/CH32FV2x_V3xRM_PDF.html - -#define FLASH_BASE 0x40022000 -#define FLASH_ACTLR (FLASH_BASE + 0) -#define FLASH_KEYR (FLASH_BASE + 4) -#define FLASH_OBKEYR (FLASH_BASE + 8) -#define FLASH_STATR (FLASH_BASE + 12) -#define FLASH_CTLR (FLASH_BASE + 16) -#define FLASH_ADDR (FLASH_BASE + 20) -#define FLASH_OBR (FLASH_BASE + 28) -#define FLASH_WPR (FLASH_BASE + 32) -void *mg_flash_start(void) { - return (void *) 0x08000000; -} -size_t mg_flash_size(void) { - return 480 * 1024; // First 320k is 0-wait -} -size_t mg_flash_sector_size(void) { - return 4096; -} -size_t mg_flash_write_align(void) { - return 4; -} -int mg_flash_bank(void) { - return 0; -} -void mg_device_reset(void) { - *((volatile uint32_t *) 0xbeef0000) |= 1U << 7; // NVIC_SystemReset() -} -static void flash_unlock(void) { - static bool unlocked; - if (unlocked == false) { - MG_REG(FLASH_KEYR) = 0x45670123; - MG_REG(FLASH_KEYR) = 0xcdef89ab; - unlocked = true; - } -} -static void flash_wait(void) { - while (MG_REG(FLASH_STATR) & MG_BIT(0)) (void) 0; -} -bool mg_flash_erase(void *addr) { - //MG_INFO(("%p", addr)); - flash_unlock(); - flash_wait(); - MG_REG(FLASH_ADDR) = (uint32_t) addr; - MG_REG(FLASH_CTLR) |= MG_BIT(1) | MG_BIT(6); // PER | STRT; - flash_wait(); - return true; -} -static bool is_page_boundary(const void *addr) { - uint32_t val = (uint32_t) addr; - return (val & (mg_flash_sector_size() - 1)) == 0; -} -bool mg_flash_write(void *addr, const void *buf, size_t len) { - //MG_INFO(("%p %p %lu", addr, buf, len)); - //mg_hexdump(buf, len); - flash_unlock(); - const uint16_t *src = (uint16_t *) buf, *end = &src[len / 2]; - uint16_t *dst = (uint16_t *) addr; - MG_REG(FLASH_CTLR) |= MG_BIT(0); // Set PG - //MG_INFO(("CTLR: %#lx", MG_REG(FLASH_CTLR))); - while (src < end) { - if (is_page_boundary(dst)) mg_flash_erase(dst); - *dst++ = *src++; - flash_wait(); - } - MG_REG(FLASH_CTLR) &= ~MG_BIT(0); // Clear PG - return true; -} -#endif -#ifdef MG_ENABLE_LINES -#line 1 "src/device_dummy.c" -#endif +struct dns_data { + struct dns_data *next; + struct mg_connection *c; + uint64_t expire; + uint16_t txnid; +}; +static void mg_sendnsreq(struct mg_connection *, struct mg_str *, int, + struct mg_dns *, bool); -#if MG_DEVICE == MG_DEVICE_NONE -void *mg_flash_start(void) { - return NULL; -} -size_t mg_flash_size(void) { - return 0; -} -size_t mg_flash_sector_size(void) { - return 0; -} -size_t mg_flash_write_align(void) { - return 0; -} -int mg_flash_bank(void) { - return 0; -} -bool mg_flash_erase(void *location) { - (void) location; - return false; +static void mg_dns_free(struct dns_data **head, struct dns_data *d) { + LIST_DELETE(struct dns_data, head, d); + free(d); } -bool mg_flash_swap_bank(void) { - return true; + +void mg_resolve_cancel(struct mg_connection *c) { + struct dns_data *tmp, *d; + struct dns_data **head = (struct dns_data **) &c->mgr->active_dns_requests; + for (d = *head; d != NULL; d = tmp) { + tmp = d->next; + if (d->c == c) mg_dns_free(head, d); + } } -bool mg_flash_write(void *addr, const void *buf, size_t len) { - (void) addr, (void) buf, (void) len; - return false; + +static size_t mg_dns_parse_name_depth(const uint8_t *s, size_t len, size_t ofs, + char *to, size_t tolen, size_t j, + int depth) { + size_t i = 0; + if (tolen > 0 && depth == 0) to[0] = '\0'; + if (depth > 5) return 0; + // MG_INFO(("ofs %lx %x %x", (unsigned long) ofs, s[ofs], s[ofs + 1])); + while (ofs + i + 1 < len) { + size_t n = s[ofs + i]; + if (n == 0) { + i++; + break; + } + if (n & 0xc0) { + size_t ptr = (((n & 0x3f) << 8) | s[ofs + i + 1]); // 12 is hdr len + // MG_INFO(("PTR %lx", (unsigned long) ptr)); + if (ptr + 1 < len && (s[ptr] & 0xc0) == 0 && + mg_dns_parse_name_depth(s, len, ptr, to, tolen, j, depth + 1) == 0) + return 0; + i += 2; + break; + } + if (ofs + i + n + 1 >= len) return 0; + if (j > 0) { + if (j < tolen) to[j] = '.'; + j++; + } + if (j + n < tolen) memcpy(&to[j], &s[ofs + i + 1], n); + j += n; + i += n + 1; + if (j < tolen) to[j] = '\0'; // Zero-terminate this chunk + // MG_INFO(("--> [%s]", to)); + } + if (tolen > 0) to[tolen - 1] = '\0'; // Make sure make sure it is nul-term + return i; } -void mg_device_reset(void) { + +static size_t mg_dns_parse_name(const uint8_t *s, size_t n, size_t ofs, + char *dst, size_t dstlen) { + return mg_dns_parse_name_depth(s, n, ofs, dst, dstlen, 0, 0); } -#endif -#ifdef MG_ENABLE_LINES -#line 1 "src/device_flash.c" -#endif +size_t mg_dns_parse_rr(const uint8_t *buf, size_t len, size_t ofs, + bool is_question, struct mg_dns_rr *rr) { + const uint8_t *s = buf + ofs, *e = &buf[len]; + memset(rr, 0, sizeof(*rr)); + if (len < sizeof(struct mg_dns_header)) return 0; // Too small + if (len > 512) return 0; // Too large, we don't expect that + if (s >= e) return 0; // Overflow -#if MG_DEVICE == MG_DEVICE_STM32H7 || MG_DEVICE == MG_DEVICE_STM32H5 || \ - MG_DEVICE == MG_DEVICE_RT1020 || MG_DEVICE == MG_DEVICE_RT1060 -// Flash can be written only if it is erased. Erased flash is 0xff (all bits 1) -// Writes must be mg_flash_write_align() - aligned. Thus if we want to save an -// object, we pad it at the end for alignment. -// -// Objects in the flash sector are stored sequentially: -// | 32-bit size | 32-bit KEY | ..data.. | ..pad.. | 32-bit size | ...... -// -// In order to get to the next object, read its size, then align up. + if ((rr->nlen = (uint16_t) mg_dns_parse_name(buf, len, ofs, NULL, 0)) == 0) + return 0; + s += rr->nlen + 4; + if (s > e) return 0; + rr->atype = (uint16_t) (((uint16_t) s[-4] << 8) | s[-3]); + rr->aclass = (uint16_t) (((uint16_t) s[-2] << 8) | s[-1]); + if (is_question) return (size_t) (rr->nlen + 4); -// Traverse the list of saved objects -size_t mg_flash_next(char *p, char *end, uint32_t *key, size_t *size) { - size_t aligned_size = 0, align = mg_flash_write_align(), left = end - p; - uint32_t *p32 = (uint32_t *) p, min_size = sizeof(uint32_t) * 2; - if (p32[0] != 0xffffffff && left > MG_ROUND_UP(min_size, align)) { - if (size) *size = (size_t) p32[0]; - if (key) *key = p32[1]; - aligned_size = MG_ROUND_UP(p32[0] + sizeof(uint32_t) * 2, align); - if (left < aligned_size) aligned_size = 0; // Out of bounds, fail - } - return aligned_size; + s += 6; + if (s > e) return 0; + rr->alen = (uint16_t) (((uint16_t) s[-2] << 8) | s[-1]); + if (s + rr->alen > e) return 0; + return (size_t) (rr->nlen + rr->alen + 10); } -// Return the last sector of Bank 2 -static char *flash_last_sector(void) { - size_t ss = mg_flash_sector_size(), size = mg_flash_size(); - char *base = (char *) mg_flash_start(), *last = base + size - ss; - if (mg_flash_bank() == 2) last -= size / 2; - return last; -} +bool mg_dns_parse(const uint8_t *buf, size_t len, struct mg_dns_message *dm) { + const struct mg_dns_header *h = (struct mg_dns_header *) buf; + struct mg_dns_rr rr; + size_t i, n, num_answers, ofs = sizeof(*h); + memset(dm, 0, sizeof(*dm)); -// Find a saved object with a given key -bool mg_flash_load(void *sector, uint32_t key, void *buf, size_t len) { - char *base = (char *) mg_flash_start(), *s = (char *) sector, *res = NULL; - size_t ss = mg_flash_sector_size(), ofs = 0, n, sz; - bool ok = false; - if (s == NULL) s = flash_last_sector(); - if (s < base || s >= base + mg_flash_size()) { - MG_ERROR(("%p is outsize of flash", sector)); - } else if (((s - base) % ss) != 0) { - MG_ERROR(("%p is not a sector boundary", sector)); - } else { - uint32_t k, scanned = 0; - while ((n = mg_flash_next(s + ofs, s + ss, &k, &sz)) > 0) { - // MG_DEBUG((" > obj %lu, ofs %lu, key %x/%x", scanned, ofs, k, key)); - // mg_hexdump(s + ofs, n); - if (k == key && sz == len) { - res = s + ofs + sizeof(uint32_t) * 2; - memcpy(buf, res, len); // Copy object - ok = true; // Keep scanning for the newer versions of it - } - ofs += n, scanned++; - } - MG_DEBUG(("Scanned %u objects, key %x is @ %p", scanned, key, res)); + if (len < sizeof(*h)) return 0; // Too small, headers dont fit + if (mg_ntohs(h->num_questions) > 1) return 0; // Sanity + num_answers = mg_ntohs(h->num_answers); + if (num_answers > 10) { + MG_DEBUG(("Got %u answers, ignoring beyond 10th one", num_answers)); + num_answers = 10; // Sanity cap } - return ok; -} + dm->txnid = mg_ntohs(h->txnid); -// For all saved objects in the sector, delete old versions of objects -static void mg_flash_sector_cleanup(char *sector) { - // Buffer all saved objects into an IO buffer (backed by RAM) - // erase sector, and re-save them. - struct mg_iobuf io = {0, 0, 0, 2048}; - size_t ss = mg_flash_sector_size(); - size_t n, size, size2, ofs = 0, hs = sizeof(uint32_t) * 2; - uint32_t key; - // Traverse all objects - MG_DEBUG(("Cleaning up sector %p", sector)); - while ((n = mg_flash_next(sector + ofs, sector + ss, &key, &size)) > 0) { - // Delete an old copy of this object in the cache - for (size_t o = 0; o < io.len; o += size2 + hs) { - uint32_t k = *(uint32_t *) (io.buf + o + sizeof(uint32_t)); - size2 = *(uint32_t *) (io.buf + o); - if (k == key) { - mg_iobuf_del(&io, o, size2 + hs); - break; - } - } - // And add the new copy - mg_iobuf_add(&io, io.len, sector + ofs, size + hs); + for (i = 0; i < mg_ntohs(h->num_questions); i++) { + if ((n = mg_dns_parse_rr(buf, len, ofs, true, &rr)) == 0) return false; + // MG_INFO(("Q %lu %lu %hu/%hu", ofs, n, rr.atype, rr.aclass)); ofs += n; } - // All objects are cached in RAM now - if (mg_flash_erase(sector)) { // Erase sector. If successful, - for (ofs = 0; ofs < io.len; ofs += size + hs) { // Traverse cached objects - size = *(uint32_t *) (io.buf + ofs); - key = *(uint32_t *) (io.buf + ofs + sizeof(uint32_t)); - mg_flash_save(sector, key, io.buf + ofs + hs, size); // Save to flash + for (i = 0; i < num_answers; i++) { + if ((n = mg_dns_parse_rr(buf, len, ofs, false, &rr)) == 0) return false; + // MG_INFO(("A -- %lu %lu %hu/%hu %s", ofs, n, rr.atype, rr.aclass, + // dm->name)); + mg_dns_parse_name(buf, len, ofs, dm->name, sizeof(dm->name)); + ofs += n; + + if (rr.alen == 4 && rr.atype == 1 && rr.aclass == 1) { + dm->addr.is_ip6 = false; + memcpy(&dm->addr.ip, &buf[ofs - 4], 4); + dm->resolved = true; + break; // Return success + } else if (rr.alen == 16 && rr.atype == 28 && rr.aclass == 1) { + dm->addr.is_ip6 = true; + memcpy(&dm->addr.ip, &buf[ofs - 16], 16); + dm->resolved = true; + break; // Return success } } - mg_iobuf_free(&io); + return true; } -// Save an object with a given key - append to the end of an object list -bool mg_flash_save(void *sector, uint32_t key, const void *buf, size_t len) { - char *base = (char *) mg_flash_start(), *s = (char *) sector; - size_t ss = mg_flash_sector_size(), ofs = 0, n; - bool ok = false; - if (s == NULL) s = flash_last_sector(); - if (s < base || s >= base + mg_flash_size()) { - MG_ERROR(("%p is outsize of flash", sector)); - } else if (((s - base) % ss) != 0) { - MG_ERROR(("%p is not a sector boundary", sector)); - } else { - char ab[mg_flash_write_align()]; // Aligned write block - uint32_t hdr[2] = {(uint32_t) len, key}; - size_t needed = sizeof(hdr) + len; - size_t needed_aligned = MG_ROUND_UP(needed, sizeof(ab)); - while ((n = mg_flash_next(s + ofs, s + ss, NULL, NULL)) > 0) ofs += n; - - // If there is not enough space left, cleanup sector and re-eval ofs - if (ofs + needed_aligned >= ss) { - mg_flash_sector_cleanup(s); - ofs = 0; - while ((n = mg_flash_next(s + ofs, s + ss, NULL, NULL)) > 0) ofs += n; +static void dns_cb(struct mg_connection *c, int ev, void *ev_data) { + struct dns_data *d, *tmp; + struct dns_data **head = (struct dns_data **) &c->mgr->active_dns_requests; + if (ev == MG_EV_POLL) { + uint64_t now = *(uint64_t *) ev_data; + for (d = *head; d != NULL; d = tmp) { + tmp = d->next; + // MG_DEBUG ("%lu %lu dns poll", d->expire, now)); + if (now > d->expire) mg_error(d->c, "DNS timeout"); } - - if (ofs + needed_aligned <= ss) { - // Enough space to save this object - if (sizeof(ab) < sizeof(hdr)) { - // Flash write granularity is 32 bit or less, write with no buffering - ok = mg_flash_write(s + ofs, hdr, sizeof(hdr)); - if (ok) mg_flash_write(s + ofs + sizeof(hdr), buf, len); - } else { - // Flash granularity is sizeof(hdr) or more. We need to save in - // 3 chunks: initial block, bulk, rest. This is because we have - // two memory chunks to write: hdr and buf, on aligned boundaries. - n = sizeof(ab) - sizeof(hdr); // Initial chunk that we write - if (n > len) n = len; // is - memset(ab, 0xff, sizeof(ab)); // initialized to all-one - memcpy(ab, hdr, sizeof(hdr)); // contains the header (key + size) - memcpy(ab + sizeof(hdr), buf, n); // and an initial part of buf - MG_INFO(("saving initial block of %lu", sizeof(ab))); - ok = mg_flash_write(s + ofs, ab, sizeof(ab)); - if (ok && len > n) { - size_t n2 = MG_ROUND_DOWN(len - n, sizeof(ab)); - if (n2 > 0) { - MG_INFO(("saving bulk, %lu", n2)); - ok = mg_flash_write(s + ofs + sizeof(ab), (char *) buf + n, n2); - } - if (ok && len > n) { - size_t n3 = len - n - n2; - if (n3 > sizeof(ab)) n3 = sizeof(ab); - memset(ab, 0xff, sizeof(ab)); - memcpy(ab, (char *) buf + n + n2, n3); - MG_INFO(("saving rest, %lu", n3)); - ok = mg_flash_write(s + ofs + sizeof(ab) + n2, ab, sizeof(ab)); + } else if (ev == MG_EV_READ) { + struct mg_dns_message dm; + int resolved = 0; + if (mg_dns_parse(c->recv.buf, c->recv.len, &dm) == false) { + MG_ERROR(("Unexpected DNS response:")); + mg_hexdump(c->recv.buf, c->recv.len); + } else { + // MG_VERBOSE(("%s %d", dm.name, dm.resolved)); + for (d = *head; d != NULL; d = tmp) { + tmp = d->next; + // MG_INFO(("d %p %hu %hu", d, d->txnid, dm.txnid)); + if (dm.txnid != d->txnid) continue; + if (d->c->is_resolving) { + if (dm.resolved) { + dm.addr.port = d->c->rem.port; // Save port + d->c->rem = dm.addr; // Copy resolved address + MG_DEBUG( + ("%lu %s is %M", d->c->id, dm.name, mg_print_ip, &d->c->rem)); + mg_connect_resolved(d->c); +#if MG_ENABLE_IPV6 + } else if (dm.addr.is_ip6 == false && dm.name[0] != '\0' && + c->mgr->use_dns6 == false) { + struct mg_str x = mg_str(dm.name); + mg_sendnsreq(d->c, &x, c->mgr->dnstimeout, &c->mgr->dns6, true); +#endif + } else { + mg_error(d->c, "%s DNS lookup failed", dm.name); } + } else { + MG_ERROR(("%lu already resolved", d->c->id)); } + mg_dns_free(head, d); + resolved = 1; } - MG_DEBUG(("Saved %lu/%lu bytes @ %p, key %x: %d", len, needed_aligned, - s + ofs, key, ok)); - MG_DEBUG(("Sector space left: %lu bytes", ss - ofs - needed_aligned)); - } else { - MG_ERROR(("Sector is full")); + } + if (!resolved) MG_ERROR(("stray DNS reply")); + c->recv.len = 0; + } else if (ev == MG_EV_CLOSE) { + for (d = *head; d != NULL; d = tmp) { + tmp = d->next; + mg_error(d->c, "DNS error"); + mg_dns_free(head, d); } } - return ok; -} -#else -bool mg_flash_save(void *sector, uint32_t key, const void *buf, size_t len) { - (void) sector, (void) key, (void) buf, (void) len; - return false; } -bool mg_flash_load(void *sector, uint32_t key, void *buf, size_t len) { - (void) sector, (void) key, (void) buf, (void) len; - return false; -} -#endif - -#ifdef MG_ENABLE_LINES -#line 1 "src/device_imxrt.c" -#endif - +static bool mg_dns_send(struct mg_connection *c, const struct mg_str *name, + uint16_t txnid, bool ipv6) { + struct { + struct mg_dns_header header; + uint8_t data[256]; + } pkt; + size_t i, n; + memset(&pkt, 0, sizeof(pkt)); + pkt.header.txnid = mg_htons(txnid); + pkt.header.flags = mg_htons(0x100); + pkt.header.num_questions = mg_htons(1); + for (i = n = 0; i < sizeof(pkt.data) - 5; i++) { + if (name->buf[i] == '.' || i >= name->len) { + pkt.data[n] = (uint8_t) (i - n); + memcpy(&pkt.data[n + 1], name->buf + n, i - n); + n = i + 1; + } + if (i >= name->len) break; + } + memcpy(&pkt.data[n], "\x00\x00\x01\x00\x01", 5); // A query + n += 5; + if (ipv6) pkt.data[n - 3] = 0x1c; // AAAA query + // memcpy(&pkt.data[n], "\xc0\x0c\x00\x1c\x00\x01", 6); // AAAA query + // n += 6; + return mg_send(c, &pkt, sizeof(pkt.header) + n); +} -#if MG_DEVICE == MG_DEVICE_RT1020 || MG_DEVICE == MG_DEVICE_RT1060 +static void mg_sendnsreq(struct mg_connection *c, struct mg_str *name, int ms, + struct mg_dns *dnsc, bool ipv6) { + struct dns_data *d = NULL; + if (dnsc->url == NULL) { + mg_error(c, "DNS server URL is NULL. Call mg_mgr_init()"); + } else if (dnsc->c == NULL) { + dnsc->c = mg_connect(c->mgr, dnsc->url, NULL, NULL); + if (dnsc->c != NULL) { + dnsc->c->pfn = dns_cb; + // dnsc->c->is_hexdumping = 1; + } + } + if (dnsc->c == NULL) { + mg_error(c, "resolver"); + } else if ((d = (struct dns_data *) calloc(1, sizeof(*d))) == NULL) { + mg_error(c, "resolve OOM"); + } else { + struct dns_data *reqs = (struct dns_data *) c->mgr->active_dns_requests; + d->txnid = reqs ? (uint16_t) (reqs->txnid + 1) : 1; + d->next = (struct dns_data *) c->mgr->active_dns_requests; + c->mgr->active_dns_requests = d; + d->expire = mg_millis() + (uint64_t) ms; + d->c = c; + c->is_resolving = 1; + MG_VERBOSE(("%lu resolving %.*s @ %s, txnid %hu", c->id, (int) name->len, + name->buf, dnsc->url, d->txnid)); + if (!mg_dns_send(dnsc->c, name, d->txnid, ipv6)) { + mg_error(dnsc->c, "DNS send"); + } + } +} -struct mg_flexspi_lut_seq { - uint8_t seqNum; - uint8_t seqId; - uint16_t reserved; -}; +void mg_resolve(struct mg_connection *c, const char *url) { + struct mg_str host = mg_url_host(url); + c->rem.port = mg_htons(mg_url_port(url)); + if (mg_aton(host, &c->rem)) { + // host is an IP address, do not fire name resolution + mg_connect_resolved(c); + } else { + // host is not an IP, send DNS resolution request + struct mg_dns *dns = c->mgr->use_dns6 ? &c->mgr->dns6 : &c->mgr->dns4; + mg_sendnsreq(c, &host, c->mgr->dnstimeout, dns, c->mgr->use_dns6); + } +} -struct mg_flexspi_mem_config { - uint32_t tag; - uint32_t version; - uint32_t reserved0; - uint8_t readSampleClkSrc; - uint8_t csHoldTime; - uint8_t csSetupTime; - uint8_t columnAddressWidth; - uint8_t deviceModeCfgEnable; - uint8_t deviceModeType; - uint16_t waitTimeCfgCommands; - struct mg_flexspi_lut_seq deviceModeSeq; - uint32_t deviceModeArg; - uint8_t configCmdEnable; - uint8_t configModeType[3]; - struct mg_flexspi_lut_seq configCmdSeqs[3]; - uint32_t reserved1; - uint32_t configCmdArgs[3]; - uint32_t reserved2; - uint32_t controllerMiscOption; - uint8_t deviceType; - uint8_t sflashPadType; - uint8_t serialClkFreq; - uint8_t lutCustomSeqEnable; - uint32_t reserved3[2]; - uint32_t sflashA1Size; - uint32_t sflashA2Size; - uint32_t sflashB1Size; - uint32_t sflashB2Size; - uint32_t csPadSettingOverride; - uint32_t sclkPadSettingOverride; - uint32_t dataPadSettingOverride; - uint32_t dqsPadSettingOverride; - uint32_t timeoutInMs; - uint32_t commandInterval; - uint16_t dataValidTime[2]; - uint16_t busyOffset; - uint16_t busyBitPolarity; - uint32_t lookupTable[64]; - struct mg_flexspi_lut_seq lutCustomSeq[12]; - uint32_t reserved4[4]; -}; +#ifdef MG_ENABLE_LINES +#line 1 "src/event.c" +#endif -struct mg_flexspi_nor_config { - struct mg_flexspi_mem_config memConfig; - uint32_t pageSize; - uint32_t sectorSize; - uint8_t ipcmdSerialClkFreq; - uint8_t isUniformBlockSize; - uint8_t reserved0[2]; - uint8_t serialNorType; - uint8_t needExitNoCmdMode; - uint8_t halfClkForNonReadCmd; - uint8_t needRestoreNoCmdMode; - uint32_t blockSize; - uint32_t reserve2[11]; -}; -/* FLEXSPI memory config block related defintions */ -#define MG_FLEXSPI_CFG_BLK_TAG (0x42464346UL) // ascii "FCFB" Big Endian -#define MG_FLEXSPI_CFG_BLK_VERSION (0x56010400UL) // V1.4.0 -#define MG_FLEXSPI_LUT_SEQ(cmd0, pad0, op0, cmd1, pad1, op1) \ - (MG_FLEXSPI_LUT_OPERAND0(op0) | MG_FLEXSPI_LUT_NUM_PADS0(pad0) | MG_FLEXSPI_LUT_OPCODE0(cmd0) | \ - MG_FLEXSPI_LUT_OPERAND1(op1) | MG_FLEXSPI_LUT_NUM_PADS1(pad1) | MG_FLEXSPI_LUT_OPCODE1(cmd1)) -#define MG_CMD_SDR 0x01 -#define MG_CMD_DDR 0x21 -#define MG_DUMMY_SDR 0x0C -#define MG_DUMMY_DDR 0x2C -#define MG_RADDR_SDR 0x02 -#define MG_RADDR_DDR 0x22 -#define MG_READ_SDR 0x09 -#define MG_READ_DDR 0x29 -#define MG_WRITE_SDR 0x08 -#define MG_WRITE_DDR 0x28 -#define MG_STOP 0 -#define MG_FLEXSPI_1PAD 0 -#define MG_FLEXSPI_2PAD 1 -#define MG_FLEXSPI_4PAD 2 -#define MG_FLEXSPI_8PAD 3 -#define MG_FLEXSPI_QSPI_LUT \ - { \ - [0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0xEB, MG_RADDR_SDR, MG_FLEXSPI_4PAD, \ - 0x18), \ - [1] = MG_FLEXSPI_LUT_SEQ(MG_DUMMY_SDR, MG_FLEXSPI_4PAD, 0x06, MG_READ_SDR, MG_FLEXSPI_4PAD, \ - 0x04), \ - [4 * 1 + 0] = \ - MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x05, MG_READ_SDR, MG_FLEXSPI_1PAD, 0x04), \ - [4 * 3 + 0] = \ - MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x06, MG_STOP, MG_FLEXSPI_1PAD, 0x0), \ - [4 * 5 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x20, MG_RADDR_SDR, \ - MG_FLEXSPI_1PAD, 0x18), \ - [4 * 8 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0xD8, MG_RADDR_SDR, \ - MG_FLEXSPI_1PAD, 0x18), \ - [4 * 9 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x02, MG_RADDR_SDR, \ - MG_FLEXSPI_1PAD, 0x18), \ - [4 * 9 + 1] = \ - MG_FLEXSPI_LUT_SEQ(MG_WRITE_SDR, MG_FLEXSPI_1PAD, 0x04, MG_STOP, MG_FLEXSPI_1PAD, 0x0), \ - [4 * 11 + 0] = \ - MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x60, MG_STOP, MG_FLEXSPI_1PAD, 0x0), \ +void mg_call(struct mg_connection *c, int ev, void *ev_data) { +#if MG_ENABLE_PROFILE + const char *names[] = { + "EV_ERROR", "EV_OPEN", "EV_POLL", "EV_RESOLVE", + "EV_CONNECT", "EV_ACCEPT", "EV_TLS_HS", "EV_READ", + "EV_WRITE", "EV_CLOSE", "EV_HTTP_MSG", "EV_HTTP_CHUNK", + "EV_WS_OPEN", "EV_WS_MSG", "EV_WS_CTL", "EV_MQTT_CMD", + "EV_MQTT_MSG", "EV_MQTT_OPEN", "EV_SNTP_TIME", "EV_USER"}; + if (ev != MG_EV_POLL && ev < (int) (sizeof(names) / sizeof(names[0]))) { + MG_PROF_ADD(c, names[ev]); } - -#define MG_FLEXSPI_LUT_OPERAND0(x) (((uint32_t) (((uint32_t) (x)))) & 0xFFU) -#define MG_FLEXSPI_LUT_NUM_PADS0(x) (((uint32_t) (((uint32_t) (x)) << 8U)) & 0x300U) -#define MG_FLEXSPI_LUT_OPCODE0(x) (((uint32_t) (((uint32_t) (x)) << 10U)) & 0xFC00U) -#define MG_FLEXSPI_LUT_OPERAND1(x) (((uint32_t) (((uint32_t) (x)) << 16U)) & 0xFF0000U) -#define MG_FLEXSPI_LUT_NUM_PADS1(x) (((uint32_t) (((uint32_t) (x)) << 24U)) & 0x3000000U) -#define MG_FLEXSPI_LUT_OPCODE1(x) (((uint32_t) (((uint32_t) (x)) << 26U)) & 0xFC000000U) - -#define FLEXSPI_NOR_INSTANCE 0 - -#if MG_DEVICE == MG_DEVICE_RT1020 -struct mg_flexspi_nor_driver_interface { - uint32_t version; - int (*init)(uint32_t instance, struct mg_flexspi_nor_config *config); - int (*program)(uint32_t instance, struct mg_flexspi_nor_config *config, uint32_t dst_addr, - const uint32_t *src); - uint32_t reserved; - int (*erase)(uint32_t instance, struct mg_flexspi_nor_config *config, uint32_t start, - uint32_t lengthInBytes); - uint32_t reserved2; - int (*update_lut)(uint32_t instance, uint32_t seqIndex, const uint32_t *lutBase, - uint32_t seqNumber); - int (*xfer)(uint32_t instance, char *xfer); - void (*clear_cache)(uint32_t instance); -}; -#elif MG_DEVICE == MG_DEVICE_RT1060 -struct mg_flexspi_nor_driver_interface { - uint32_t version; - int (*init)(uint32_t instance, struct mg_flexspi_nor_config *config); - int (*program)(uint32_t instance, struct mg_flexspi_nor_config *config, uint32_t dst_addr, - const uint32_t *src); - int (*erase_all)(uint32_t instance, struct mg_flexspi_nor_config *config); - int (*erase)(uint32_t instance, struct mg_flexspi_nor_config *config, uint32_t start, - uint32_t lengthInBytes); - int (*read)(uint32_t instance, struct mg_flexspi_nor_config *config, uint32_t *dst, uint32_t addr, - uint32_t lengthInBytes); - void (*clear_cache)(uint32_t instance); - int (*xfer)(uint32_t instance, char *xfer); - int (*update_lut)(uint32_t instance, uint32_t seqIndex, const uint32_t *lutBase, - uint32_t seqNumber); - int (*get_config)(uint32_t instance, struct mg_flexspi_nor_config *config, uint32_t *option); -}; #endif - -#define flexspi_nor (*((struct mg_flexspi_nor_driver_interface**) \ - (*(uint32_t*)0x0020001c + 16))) - -static bool s_flash_irq_disabled; - -MG_IRAM void *mg_flash_start(void) { - return (void *) 0x60000000; -} -MG_IRAM size_t mg_flash_size(void) { - return 8 * 1024 * 1024; -} -MG_IRAM size_t mg_flash_sector_size(void) { - return 4 * 1024; // 4k -} -MG_IRAM size_t mg_flash_write_align(void) { - return 256; -} -MG_IRAM int mg_flash_bank(void) { - return 0; -} - -MG_IRAM static bool flash_page_start(volatile uint32_t *dst) { - char *base = (char *) mg_flash_start(), *end = base + mg_flash_size(); - volatile char *p = (char *) dst; - return p >= base && p < end && ((p - base) % mg_flash_sector_size()) == 0; + // Fire protocol handler first, user handler second. See #2559 + if (c->pfn != NULL) c->pfn(c, ev, ev_data); + if (c->fn != NULL) c->fn(c, ev, ev_data); } -// Note: the get_config function below works both for RT1020 and 1060 -#if MG_DEVICE == MG_DEVICE_RT1020 -MG_IRAM static int flexspi_nor_get_config(struct mg_flexspi_nor_config *config) { - struct mg_flexspi_nor_config default_config = { - .memConfig = {.tag = MG_FLEXSPI_CFG_BLK_TAG, - .version = MG_FLEXSPI_CFG_BLK_VERSION, - .readSampleClkSrc = 1, // ReadSampleClk_LoopbackFromDqsPad - .csHoldTime = 3, - .csSetupTime = 3, - .controllerMiscOption = MG_BIT(4), - .deviceType = 1, // serial NOR - .sflashPadType = 4, - .serialClkFreq = 7, // 133MHz - .sflashA1Size = 8 * 1024 * 1024, - .lookupTable = MG_FLEXSPI_QSPI_LUT}, - .pageSize = 256, - .sectorSize = 4 * 1024, - .ipcmdSerialClkFreq = 1, - .blockSize = 64 * 1024, - .isUniformBlockSize = false}; - - *config = default_config; - return 0; +void mg_error(struct mg_connection *c, const char *fmt, ...) { + char buf[64]; + va_list ap; + va_start(ap, fmt); + mg_vsnprintf(buf, sizeof(buf), fmt, &ap); + va_end(ap); + MG_ERROR(("%lu %ld %s", c->id, c->fd, buf)); + c->is_closing = 1; // Set is_closing before sending MG_EV_CALL + mg_call(c, MG_EV_ERROR, buf); // Let user handler override it } -#else -MG_IRAM static int flexspi_nor_get_config(struct mg_flexspi_nor_config *config) { - uint32_t options[] = {0xc0000000, 0x00}; - MG_ARM_DISABLE_IRQ(); - uint32_t status = - flexspi_nor->get_config(FLEXSPI_NOR_INSTANCE, config, options); - if (!s_flash_irq_disabled) { - MG_ARM_ENABLE_IRQ(); - } - if (status) { - MG_ERROR(("Failed to extract flash configuration: status %u", status)); - } - return status; -} +#ifdef MG_ENABLE_LINES +#line 1 "src/flash.c" #endif -MG_IRAM bool mg_flash_erase(void *addr) { - struct mg_flexspi_nor_config config; - if (flexspi_nor_get_config(&config) != 0) { - return false; - } - if (flash_page_start(addr) == false) { - MG_ERROR(("%p is not on a sector boundary", addr)); - return false; - } - void *dst = (void *)((char *) addr - (char *) mg_flash_start()); - // Note: Interrupts must be disabled before any call to the ROM API on RT1020 - // and 1060 - MG_ARM_DISABLE_IRQ(); - bool ok = (flexspi_nor->erase(FLEXSPI_NOR_INSTANCE, &config, (uint32_t) dst, - mg_flash_sector_size()) == 0); - if (!s_flash_irq_disabled) { - MG_ARM_ENABLE_IRQ(); // Reenable them after the call - } - MG_DEBUG(("Sector starting at %p erasure: %s", addr, ok ? "ok" : "fail")); - return ok; -} -MG_IRAM bool mg_flash_swap_bank(void) { - return true; -} -static inline void spin(volatile uint32_t count) { - while (count--) (void) 0; -} +#if MG_OTA != MG_OTA_NONE && MG_OTA != MG_OTA_CUSTOM -static inline void flash_wait(void) { - while ((*((volatile uint32_t *)(0x402A8000 + 0xE0)) & MG_BIT(1)) == 0) - spin(1); -} +static char *s_addr; // Current address to write to +static size_t s_size; // Firmware size to flash. In-progress indicator +static uint32_t s_crc32; // Firmware checksum -MG_IRAM static void *flash_code_location(void) { - return (void *) ((char *) mg_flash_start() + 0x2000); +bool mg_ota_flash_begin(size_t new_firmware_size, struct mg_flash *flash) { + bool ok = false; + if (s_size) { + MG_ERROR(("OTA already in progress. Call mg_ota_end()")); + } else { + size_t half = flash->size / 2; + s_crc32 = 0; + s_addr = (char *) flash->start + half; + MG_DEBUG(("FW %lu bytes, max %lu", new_firmware_size, half)); + if (new_firmware_size < half) { + ok = true; + s_size = new_firmware_size; + MG_INFO(("Starting OTA, firmware size %lu", s_size)); + } else { + MG_ERROR(("Firmware %lu is too big to fit %lu", new_firmware_size, half)); + } + } + return ok; } -MG_IRAM bool mg_flash_write(void *addr, const void *buf, size_t len) { - struct mg_flexspi_nor_config config; - if (flexspi_nor_get_config(&config) != 0) { - return false; - } - if ((len % mg_flash_write_align()) != 0) { - MG_ERROR(("%lu is not aligned to %lu", len, mg_flash_write_align())); - return false; - } - - if ((char *) addr < (char *) mg_flash_start()) { - MG_ERROR(("Invalid flash write address: %p", addr)); - return false; - } - - uint32_t *dst = (uint32_t *) addr; - uint32_t *src = (uint32_t *) buf; - uint32_t *end = (uint32_t *) ((char *) buf + len); - bool ok = true; - - // Note: If we overwrite the flash irq section of the image, we must also - // make sure interrupts are disabled and are not reenabled until we write - // this sector with another irq table. - if ((char *) addr == (char *) flash_code_location()) { - s_flash_irq_disabled = true; - MG_ARM_DISABLE_IRQ(); - } - - while (ok && src < end) { - if (flash_page_start(dst) && mg_flash_erase(dst) == false) { - break; - } - uint32_t status; - uint32_t dst_ofs = (uint32_t) dst - (uint32_t) mg_flash_start(); - if ((char *) buf >= (char *) mg_flash_start()) { - // If we copy from FLASH to FLASH, then we first need to copy the source - // to RAM - size_t tmp_buf_size = mg_flash_write_align() / sizeof(uint32_t); - uint32_t tmp[tmp_buf_size]; - - for (size_t i = 0; i < tmp_buf_size; i++) { - flash_wait(); - tmp[i] = src[i]; - } - MG_ARM_DISABLE_IRQ(); - status = flexspi_nor->program(FLEXSPI_NOR_INSTANCE, &config, - (uint32_t) dst_ofs, tmp); - } else { - MG_ARM_DISABLE_IRQ(); - status = flexspi_nor->program(FLEXSPI_NOR_INSTANCE, &config, - (uint32_t) dst_ofs, src); - } - if (!s_flash_irq_disabled) { - MG_ARM_ENABLE_IRQ(); - } - src = (uint32_t *) ((char *) src + mg_flash_write_align()); - dst = (uint32_t *) ((char *) dst + mg_flash_write_align()); - if (status != 0) { - ok = false; +bool mg_ota_flash_write(const void *buf, size_t len, struct mg_flash *flash) { + bool ok = false; + if (s_size == 0) { + MG_ERROR(("OTA is not started, call mg_ota_begin()")); + } else { + size_t len_aligned_down = MG_ROUND_DOWN(len, flash->align); + if (len_aligned_down) ok = flash->write_fn(s_addr, buf, len_aligned_down); + if (len_aligned_down < len) { + size_t left = len - len_aligned_down; + char tmp[flash->align]; + memset(tmp, 0xff, sizeof(tmp)); + memcpy(tmp, (char *) buf + len_aligned_down, left); + ok = flash->write_fn(s_addr + len_aligned_down, tmp, sizeof(tmp)); } + s_crc32 = mg_crc32(s_crc32, (char *) buf, len); // Update CRC + MG_DEBUG(("%#x %p %lu -> %d", s_addr - len, buf, len, ok)); + s_addr += len; } - MG_DEBUG(("Flash write %lu bytes @ %p: %s.", len, dst, ok ? "ok" : "fail")); return ok; } -MG_IRAM void mg_device_reset(void) { - MG_DEBUG(("Resetting device...")); - *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; +bool mg_ota_flash_end(struct mg_flash *flash) { + char *base = (char *) flash->start + flash->size / 2; + bool ok = false; + if (s_size) { + size_t size = (size_t) (s_addr - base); + uint32_t crc32 = mg_crc32(0, base, s_size); + if (size == s_size && crc32 == s_crc32) ok = true; + MG_DEBUG(("CRC: %x/%x, size: %lu/%lu, status: %s", s_crc32, crc32, s_size, + size, ok ? "ok" : "fail")); + s_size = 0; + if (ok) ok = flash->swap_fn(); + } + MG_INFO(("Finishing OTA: %s", ok ? "ok" : "fail")); + return ok; } #endif #ifdef MG_ENABLE_LINES -#line 1 "src/device_stm32h5.c" +#line 1 "src/fmt.c" #endif -#if MG_DEVICE == MG_DEVICE_STM32H5 - -#define FLASH_BASE 0x40022000 // Base address of the flash controller -#define FLASH_KEYR (FLASH_BASE + 0x4) // See RM0481 7.11 -#define FLASH_OPTKEYR (FLASH_BASE + 0xc) -#define FLASH_OPTCR (FLASH_BASE + 0x1c) -#define FLASH_NSSR (FLASH_BASE + 0x20) -#define FLASH_NSCR (FLASH_BASE + 0x28) -#define FLASH_NSCCR (FLASH_BASE + 0x30) -#define FLASH_OPTSR_CUR (FLASH_BASE + 0x50) -#define FLASH_OPTSR_PRG (FLASH_BASE + 0x54) -void *mg_flash_start(void) { - return (void *) 0x08000000; -} -size_t mg_flash_size(void) { - return 2 * 1024 * 1024; // 2Mb -} -size_t mg_flash_sector_size(void) { - return 8 * 1024; // 8k -} -size_t mg_flash_write_align(void) { - return 16; // 128 bit -} -int mg_flash_bank(void) { - return MG_REG(FLASH_OPTCR) & MG_BIT(31) ? 2 : 1; +static bool is_digit(int c) { + return c >= '0' && c <= '9'; } -static void flash_unlock(void) { - static bool unlocked = false; - if (unlocked == false) { - MG_REG(FLASH_KEYR) = 0x45670123; - MG_REG(FLASH_KEYR) = 0Xcdef89ab; - MG_REG(FLASH_OPTKEYR) = 0x08192a3b; - MG_REG(FLASH_OPTKEYR) = 0x4c5d6e7f; - unlocked = true; - } +static int addexp(char *buf, int e, int sign) { + int n = 0; + buf[n++] = 'e'; + buf[n++] = (char) sign; + if (e > 400) return 0; + if (e < 10) buf[n++] = '0'; + if (e >= 100) buf[n++] = (char) (e / 100 + '0'), e -= 100 * (e / 100); + if (e >= 10) buf[n++] = (char) (e / 10 + '0'), e -= 10 * (e / 10); + buf[n++] = (char) (e + '0'); + return n; } -static int flash_page_start(volatile uint32_t *dst) { - char *base = (char *) mg_flash_start(), *end = base + mg_flash_size(); - volatile char *p = (char *) dst; - return p >= base && p < end && ((p - base) % mg_flash_sector_size()) == 0; +static int xisinf(double x) { + union { + double f; + uint64_t u; + } ieee754 = {x}; + return ((unsigned) (ieee754.u >> 32) & 0x7fffffff) == 0x7ff00000 && + ((unsigned) ieee754.u == 0); } -static bool flash_is_err(void) { - return MG_REG(FLASH_NSSR) & ((MG_BIT(8) - 1) << 17); // RM0481 7.11.9 +static int xisnan(double x) { + union { + double f; + uint64_t u; + } ieee754 = {x}; + return ((unsigned) (ieee754.u >> 32) & 0x7fffffff) + + ((unsigned) ieee754.u != 0) > + 0x7ff00000; } -static void flash_wait(void) { - while ((MG_REG(FLASH_NSSR) & MG_BIT(0)) && - (MG_REG(FLASH_NSSR) & MG_BIT(16)) == 0) { - (void) 0; +static size_t mg_dtoa(char *dst, size_t dstlen, double d, int width, bool tz) { + char buf[40]; + int i, s = 0, n = 0, e = 0; + double t, mul, saved; + if (d == 0.0) return mg_snprintf(dst, dstlen, "%s", "0"); + if (xisinf(d)) return mg_snprintf(dst, dstlen, "%s", d > 0 ? "inf" : "-inf"); + if (xisnan(d)) return mg_snprintf(dst, dstlen, "%s", "nan"); + if (d < 0.0) d = -d, buf[s++] = '-'; + + // Round + saved = d; + if (tz) { + mul = 1.0; + while (d >= 10.0 && d / mul >= 10.0) mul *= 10.0; + } else { + mul = 0.1; } -} -static void flash_clear_err(void) { - flash_wait(); // Wait until ready - MG_REG(FLASH_NSCCR) = ((MG_BIT(9) - 1) << 16U); // Clear all errors -} + while (d <= 1.0 && d / mul <= 1.0) mul /= 10.0; + for (i = 0, t = mul * 5; i < width; i++) t /= 10.0; -static bool flash_bank_is_swapped(void) { - return MG_REG(FLASH_OPTCR) & MG_BIT(31); // RM0481 7.11.8 -} + d += t; -bool mg_flash_erase(void *location) { - bool ok = false; - if (flash_page_start(location) == false) { - MG_ERROR(("%p is not on a sector boundary")); + // Calculate exponent, and 'mul' for scientific representation + mul = 1.0; + while (d >= 10.0 && d / mul >= 10.0) mul *= 10.0, e++; + while (d < 1.0 && d / mul < 1.0) mul /= 10.0, e--; + // printf(" --> %g %d %g %g\n", saved, e, t, mul); + + if (tz && e >= width && width > 1) { + n = (int) mg_dtoa(buf, sizeof(buf), saved / mul, width, tz); + // printf(" --> %.*g %d [%.*s]\n", 10, d / t, e, n, buf); + n += addexp(buf + s + n, e, '+'); + return mg_snprintf(dst, dstlen, "%.*s", n, buf); + } else if (tz && e <= -width && width > 1) { + n = (int) mg_dtoa(buf, sizeof(buf), saved / mul, width, tz); + // printf(" --> %.*g %d [%.*s]\n", 10, d / mul, e, n, buf); + n += addexp(buf + s + n, -e, '-'); + return mg_snprintf(dst, dstlen, "%.*s", n, buf); } else { - uintptr_t diff = (char *) location - (char *) mg_flash_start(); - uint32_t sector = diff / mg_flash_sector_size(); - uint32_t saved_cr = MG_REG(FLASH_NSCR); // Save CR value - flash_unlock(); - flash_clear_err(); - MG_REG(FLASH_NSCR) = 0; - if ((sector < 128 && flash_bank_is_swapped()) || - (sector > 127 && !flash_bank_is_swapped())) { - MG_REG(FLASH_NSCR) |= MG_BIT(31); // Set FLASH_CR_BKSEL + int targ_width = width; + for (i = 0, t = mul; t >= 1.0 && s + n < (int) sizeof(buf); i++) { + int ch = (int) (d / t); + if (n > 0 || ch > 0) buf[s + n++] = (char) (ch + '0'); + d -= ch * t; + t /= 10.0; + } + // printf(" --> [%g] -> %g %g (%d) [%.*s]\n", saved, d, t, n, s + n, buf); + if (n == 0) buf[s++] = '0'; + while (t >= 1.0 && n + s < (int) sizeof(buf)) buf[n++] = '0', t /= 10.0; + if (s + n < (int) sizeof(buf)) buf[n + s++] = '.'; + // printf(" 1--> [%g] -> [%.*s]\n", saved, s + n, buf); + if (!tz && n > 0) targ_width = width + n; + for (i = 0, t = 0.1; s + n < (int) sizeof(buf) && n < targ_width; i++) { + int ch = (int) (d / t); + buf[s + n++] = (char) (ch + '0'); + d -= ch * t; + t /= 10.0; } - if (sector > 127) sector -= 128; - MG_REG(FLASH_NSCR) |= MG_BIT(2) | (sector << 6); // Erase | sector_num - MG_REG(FLASH_NSCR) |= MG_BIT(5); // Start erasing - flash_wait(); - ok = !flash_is_err(); - MG_DEBUG(("Erase sector %lu @ %p: %s. CR %#lx SR %#lx", sector, location, - ok ? "ok" : "fail", MG_REG(FLASH_NSCR), MG_REG(FLASH_NSSR))); - // mg_hexdump(location, 32); - MG_REG(FLASH_NSCR) = saved_cr; // Restore saved CR } - return ok; -} -bool mg_flash_swap_bank(void) { - uint32_t desired = flash_bank_is_swapped() ? 0 : MG_BIT(31); - flash_unlock(); - flash_clear_err(); - // printf("OPTSR_PRG 1 %#lx\n", FLASH->OPTSR_PRG); - MG_SET_BITS(MG_REG(FLASH_OPTSR_PRG), MG_BIT(31), desired); - // printf("OPTSR_PRG 2 %#lx\n", FLASH->OPTSR_PRG); - MG_REG(FLASH_OPTCR) |= MG_BIT(1); // OPTSTART - while ((MG_REG(FLASH_OPTSR_CUR) & MG_BIT(31)) != desired) (void) 0; - return true; + while (tz && n > 0 && buf[s + n - 1] == '0') n--; // Trim trailing zeroes + if (tz && n > 0 && buf[s + n - 1] == '.') n--; // Trim trailing dot + n += s; + if (n >= (int) sizeof(buf)) n = (int) sizeof(buf) - 1; + buf[n] = '\0'; + return mg_snprintf(dst, dstlen, "%s", buf); } -bool mg_flash_write(void *addr, const void *buf, size_t len) { - if ((len % mg_flash_write_align()) != 0) { - MG_ERROR(("%lu is not aligned to %lu", len, mg_flash_write_align())); - return false; +static size_t mg_lld(char *buf, int64_t val, bool is_signed, bool is_hex) { + const char *letters = "0123456789abcdef"; + uint64_t v = (uint64_t) val; + size_t s = 0, n, i; + if (is_signed && val < 0) buf[s++] = '-', v = (uint64_t) (-val); + // This loop prints a number in reverse order. I guess this is because we + // write numbers from right to left: least significant digit comes last. + // Maybe because we use Arabic numbers, and Arabs write RTL? + if (is_hex) { + for (n = 0; v; v >>= 4) buf[s + n++] = letters[v & 15]; + } else { + for (n = 0; v; v /= 10) buf[s + n++] = letters[v % 10]; } - uint32_t *dst = (uint32_t *) addr; - uint32_t *src = (uint32_t *) buf; - uint32_t *end = (uint32_t *) ((char *) buf + len); - bool ok = true; - flash_unlock(); - flash_clear_err(); - MG_ARM_DISABLE_IRQ(); - // MG_DEBUG(("Starting flash write %lu bytes @ %p", len, addr)); - MG_REG(FLASH_NSCR) = MG_BIT(1); // Set programming flag - while (ok && src < end) { - if (flash_page_start(dst) && mg_flash_erase(dst) == false) break; - *(volatile uint32_t *) dst++ = *src++; - flash_wait(); - if (flash_is_err()) ok = false; + // Reverse a string + for (i = 0; i < n / 2; i++) { + char t = buf[s + i]; + buf[s + i] = buf[s + n - i - 1], buf[s + n - i - 1] = t; } - MG_ARM_ENABLE_IRQ(); - MG_DEBUG(("Flash write %lu bytes @ %p: %s. CR %#lx SR %#lx", len, dst, - flash_is_err() ? "fail" : "ok", MG_REG(FLASH_NSCR), - MG_REG(FLASH_NSSR))); - MG_REG(FLASH_NSCR) = 0; // Clear flags - return ok; + if (val == 0) buf[n++] = '0'; // Handle special case + return n + s; } -void mg_device_reset(void) { - // SCB->AIRCR = ((0x5fa << SCB_AIRCR_VECTKEY_Pos)|SCB_AIRCR_SYSRESETREQ_Msk); - *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; +static size_t scpy(void (*out)(char, void *), void *ptr, char *buf, + size_t len) { + size_t i = 0; + while (i < len && buf[i] != '\0') out(buf[i++], ptr); + return i; } -#endif -#ifdef MG_ENABLE_LINES -#line 1 "src/device_stm32h7.c" -#endif +size_t mg_xprintf(void (*out)(char, void *), void *ptr, const char *fmt, ...) { + size_t len = 0; + va_list ap; + va_start(ap, fmt); + len = mg_vxprintf(out, ptr, fmt, &ap); + va_end(ap); + return len; +} +size_t mg_vxprintf(void (*out)(char, void *), void *param, const char *fmt, + va_list *ap) { + size_t i = 0, n = 0; + while (fmt[i] != '\0') { + if (fmt[i] == '%') { + size_t j, k, x = 0, is_long = 0, w = 0 /* width */, pr = ~0U /* prec */; + char pad = ' ', minus = 0, c = fmt[++i]; + if (c == '#') x++, c = fmt[++i]; + if (c == '-') minus++, c = fmt[++i]; + if (c == '0') pad = '0', c = fmt[++i]; + while (is_digit(c)) w *= 10, w += (size_t) (c - '0'), c = fmt[++i]; + if (c == '.') { + c = fmt[++i]; + if (c == '*') { + pr = (size_t) va_arg(*ap, int); + c = fmt[++i]; + } else { + pr = 0; + while (is_digit(c)) pr *= 10, pr += (size_t) (c - '0'), c = fmt[++i]; + } + } + while (c == 'h') c = fmt[++i]; // Treat h and hh as int + if (c == 'l') { + is_long++, c = fmt[++i]; + if (c == 'l') is_long++, c = fmt[++i]; + } + if (c == 'p') x = 1, is_long = 1; + if (c == 'd' || c == 'u' || c == 'x' || c == 'X' || c == 'p' || + c == 'g' || c == 'f') { + bool s = (c == 'd'), h = (c == 'x' || c == 'X' || c == 'p'); + char tmp[40]; + size_t xl = x ? 2 : 0; + if (c == 'g' || c == 'f') { + double v = va_arg(*ap, double); + if (pr == ~0U) pr = 6; + k = mg_dtoa(tmp, sizeof(tmp), v, (int) pr, c == 'g'); + } else if (is_long == 2) { + int64_t v = va_arg(*ap, int64_t); + k = mg_lld(tmp, v, s, h); + } else if (is_long == 1) { + long v = va_arg(*ap, long); + k = mg_lld(tmp, s ? (int64_t) v : (int64_t) (unsigned long) v, s, h); + } else { + int v = va_arg(*ap, int); + k = mg_lld(tmp, s ? (int64_t) v : (int64_t) (unsigned) v, s, h); + } + for (j = 0; j < xl && w > 0; j++) w--; + for (j = 0; pad == ' ' && !minus && k < w && j + k < w; j++) + n += scpy(out, param, &pad, 1); + n += scpy(out, param, (char *) "0x", xl); + for (j = 0; pad == '0' && k < w && j + k < w; j++) + n += scpy(out, param, &pad, 1); + n += scpy(out, param, tmp, k); + for (j = 0; pad == ' ' && minus && k < w && j + k < w; j++) + n += scpy(out, param, &pad, 1); + } else if (c == 'm' || c == 'M') { + mg_pm_t f = va_arg(*ap, mg_pm_t); + if (c == 'm') out('"', param); + n += f(out, param, ap); + if (c == 'm') n += 2, out('"', param); + } else if (c == 'c') { + int ch = va_arg(*ap, int); + out((char) ch, param); + n++; + } else if (c == 's') { + char *p = va_arg(*ap, char *); + if (pr == ~0U) pr = p == NULL ? 0 : strlen(p); + for (j = 0; !minus && pr < w && j + pr < w; j++) + n += scpy(out, param, &pad, 1); + n += scpy(out, param, p, pr); + for (j = 0; minus && pr < w && j + pr < w; j++) + n += scpy(out, param, &pad, 1); + } else if (c == '%') { + out('%', param); + n++; + } else { + out('%', param); + out(c, param); + n += 2; + } + i++; + } else { + out(fmt[i], param), n++, i++; + } + } + return n; +} +#ifdef MG_ENABLE_LINES +#line 1 "src/fs.c" +#endif -#if MG_DEVICE == MG_DEVICE_STM32H7 -#define FLASH_BASE1 0x52002000 // Base address for bank1 -#define FLASH_BASE2 0x52002100 // Base address for bank2 -#define FLASH_KEYR 0x04 // See RM0433 4.9.2 -#define FLASH_OPTKEYR 0x08 -#define FLASH_OPTCR 0x18 -#define FLASH_SR 0x10 -#define FLASH_CR 0x0c -#define FLASH_CCR 0x14 -#define FLASH_OPTSR_CUR 0x1c -#define FLASH_OPTSR_PRG 0x20 -#define FLASH_SIZE_REG 0x1ff1e880 -MG_IRAM void *mg_flash_start(void) { - return (void *) 0x08000000; -} -MG_IRAM size_t mg_flash_size(void) { - return MG_REG(FLASH_SIZE_REG) * 1024; -} -MG_IRAM size_t mg_flash_sector_size(void) { - return 128 * 1024; // 128k -} -MG_IRAM size_t mg_flash_write_align(void) { - return 32; // 256 bit -} -MG_IRAM int mg_flash_bank(void) { - if (mg_flash_size() < 2 * 1024 * 1024) return 0; // No dual bank support - return MG_REG(FLASH_BASE1 + FLASH_OPTCR) & MG_BIT(31) ? 2 : 1; -} -MG_IRAM static void flash_unlock(void) { - static bool unlocked = false; - if (unlocked == false) { - MG_REG(FLASH_BASE1 + FLASH_KEYR) = 0x45670123; - MG_REG(FLASH_BASE1 + FLASH_KEYR) = 0xcdef89ab; - if (mg_flash_bank() > 0) { - MG_REG(FLASH_BASE2 + FLASH_KEYR) = 0x45670123; - MG_REG(FLASH_BASE2 + FLASH_KEYR) = 0xcdef89ab; +struct mg_fd *mg_fs_open(struct mg_fs *fs, const char *path, int flags) { + struct mg_fd *fd = (struct mg_fd *) calloc(1, sizeof(*fd)); + if (fd != NULL) { + fd->fd = fs->op(path, flags); + fd->fs = fs; + if (fd->fd == NULL) { + free(fd); + fd = NULL; } - MG_REG(FLASH_BASE1 + FLASH_OPTKEYR) = 0x08192a3b; // opt reg is "shared" - MG_REG(FLASH_BASE1 + FLASH_OPTKEYR) = 0x4c5d6e7f; // thus unlock once - unlocked = true; } + return fd; } -MG_IRAM static bool flash_page_start(volatile uint32_t *dst) { - char *base = (char *) mg_flash_start(), *end = base + mg_flash_size(); - volatile char *p = (char *) dst; - return p >= base && p < end && ((p - base) % mg_flash_sector_size()) == 0; +void mg_fs_close(struct mg_fd *fd) { + if (fd != NULL) { + fd->fs->cl(fd->fd); + free(fd); + } } -MG_IRAM static bool flash_is_err(uint32_t bank) { - return MG_REG(bank + FLASH_SR) & ((MG_BIT(11) - 1) << 17); // RM0433 4.9.5 +struct mg_str mg_file_read(struct mg_fs *fs, const char *path) { + struct mg_str result = {NULL, 0}; + void *fp; + fs->st(path, &result.len, NULL); + if ((fp = fs->op(path, MG_FS_READ)) != NULL) { + result.buf = (char *) calloc(1, result.len + 1); + if (result.buf != NULL && + fs->rd(fp, (void *) result.buf, result.len) != result.len) { + free((void *) result.buf); + result.buf = NULL; + } + fs->cl(fp); + } + if (result.buf == NULL) result.len = 0; + return result; } -MG_IRAM static void flash_wait(uint32_t bank) { - while (MG_REG(bank + FLASH_SR) & (MG_BIT(0) | MG_BIT(2))) (void) 0; +bool mg_file_write(struct mg_fs *fs, const char *path, const void *buf, + size_t len) { + bool result = false; + struct mg_fd *fd; + char tmp[MG_PATH_MAX]; + mg_snprintf(tmp, sizeof(tmp), "%s..%d", path, rand()); + if ((fd = mg_fs_open(fs, tmp, MG_FS_WRITE)) != NULL) { + result = fs->wr(fd->fd, buf, len) == len; + mg_fs_close(fd); + if (result) { + fs->rm(path); + fs->mv(tmp, path); + } else { + fs->rm(tmp); + } + } + return result; } -MG_IRAM static void flash_clear_err(uint32_t bank) { - flash_wait(bank); // Wait until ready - MG_REG(bank + FLASH_CCR) = ((MG_BIT(11) - 1) << 16U); // Clear all errors +bool mg_file_printf(struct mg_fs *fs, const char *path, const char *fmt, ...) { + va_list ap; + char *data; + bool result = false; + va_start(ap, fmt); + data = mg_vmprintf(fmt, &ap); + va_end(ap); + result = mg_file_write(fs, path, data, strlen(data)); + free(data); + return result; } -MG_IRAM static bool flash_bank_is_swapped(uint32_t bank) { - return MG_REG(bank + FLASH_OPTCR) & MG_BIT(31); // RM0433 4.9.7 +// This helper function allows to scan a filesystem in a sequential way, +// without using callback function: +// char buf[100] = ""; +// while (mg_fs_ls(&mg_fs_posix, "./", buf, sizeof(buf))) { +// ... +static void mg_fs_ls_fn(const char *filename, void *param) { + struct mg_str *s = (struct mg_str *) param; + if (s->buf[0] == '\0') { + mg_snprintf((char *) s->buf, s->len, "%s", filename); + } else if (strcmp(s->buf, filename) == 0) { + ((char *) s->buf)[0] = '\0'; // Fetch next file + } } -// Figure out flash bank based on the address -MG_IRAM static uint32_t flash_bank(void *addr) { - size_t ofs = (char *) addr - (char *) mg_flash_start(); - if (mg_flash_bank() == 0) return FLASH_BASE1; - return ofs < mg_flash_size() / 2 ? FLASH_BASE1 : FLASH_BASE2; +bool mg_fs_ls(struct mg_fs *fs, const char *path, char *buf, size_t len) { + struct mg_str s = {buf, len}; + fs->ls(path, mg_fs_ls_fn, &s); + return buf[0] != '\0'; } -MG_IRAM bool mg_flash_erase(void *addr) { - bool ok = false; - if (flash_page_start(addr) == false) { - MG_ERROR(("%p is not on a sector boundary", addr)); - } else { - uintptr_t diff = (char *) addr - (char *) mg_flash_start(); - uint32_t sector = diff / mg_flash_sector_size(); - uint32_t bank = flash_bank(addr); - uint32_t saved_cr = MG_REG(bank + FLASH_CR); // Save CR value +#ifdef MG_ENABLE_LINES +#line 1 "src/fs_fat.c" +#endif - flash_unlock(); - if (sector > 7) sector -= 8; - flash_clear_err(bank); - MG_REG(bank + FLASH_CR) = MG_BIT(5); // 32-bit write parallelism - MG_REG(bank + FLASH_CR) |= (sector & 7U) << 8U; // Sector to erase - MG_REG(bank + FLASH_CR) |= MG_BIT(2); // Sector erase bit - MG_REG(bank + FLASH_CR) |= MG_BIT(7); // Start erasing - ok = !flash_is_err(bank); - MG_DEBUG(("Erase sector %lu @ %p %s. CR %#lx SR %#lx", sector, addr, - ok ? "ok" : "fail", MG_REG(bank + FLASH_CR), - MG_REG(bank + FLASH_SR))); - MG_REG(bank + FLASH_CR) = saved_cr; // Restore CR - } - return ok; -} -MG_IRAM bool mg_flash_swap_bank(void) { - if (mg_flash_bank() == 0) return true; - uint32_t bank = FLASH_BASE1; - uint32_t desired = flash_bank_is_swapped(bank) ? 0 : MG_BIT(31); - flash_unlock(); - flash_clear_err(bank); - // printf("OPTSR_PRG 1 %#lx\n", FLASH->OPTSR_PRG); - MG_SET_BITS(MG_REG(bank + FLASH_OPTSR_PRG), MG_BIT(31), desired); - // printf("OPTSR_PRG 2 %#lx\n", FLASH->OPTSR_PRG); - MG_REG(bank + FLASH_OPTCR) |= MG_BIT(1); // OPTSTART - while ((MG_REG(bank + FLASH_OPTSR_CUR) & MG_BIT(31)) != desired) (void) 0; - return true; +#if MG_ENABLE_FATFS +#include + +static int mg_days_from_epoch(int y, int m, int d) { + y -= m <= 2; + int era = y / 400; + int yoe = y - era * 400; + int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + doe - 719468; } -MG_IRAM bool mg_flash_write(void *addr, const void *buf, size_t len) { - if ((len % mg_flash_write_align()) != 0) { - MG_ERROR(("%lu is not aligned to %lu", len, mg_flash_write_align())); - return false; +static time_t mg_timegm(const struct tm *t) { + int year = t->tm_year + 1900; + int month = t->tm_mon; // 0-11 + if (month > 11) { + year += month / 12; + month %= 12; + } else if (month < 0) { + int years_diff = (11 - month) / 12; + year -= years_diff; + month += 12 * years_diff; } - uint32_t bank = flash_bank(addr); - uint32_t *dst = (uint32_t *) addr; - uint32_t *src = (uint32_t *) buf; - uint32_t *end = (uint32_t *) ((char *) buf + len); - bool ok = true; - flash_unlock(); - flash_clear_err(bank); - MG_REG(bank + FLASH_CR) = MG_BIT(1); // Set programming flag - MG_REG(bank + FLASH_CR) |= MG_BIT(5); // 32-bit write parallelism - MG_DEBUG(("Writing flash @ %p, %lu bytes", addr, len)); - MG_ARM_DISABLE_IRQ(); - while (ok && src < end) { - if (flash_page_start(dst) && mg_flash_erase(dst) == false) break; - *(volatile uint32_t *) dst++ = *src++; - flash_wait(bank); - if (flash_is_err(bank)) ok = false; + int x = mg_days_from_epoch(year, month + 1, t->tm_mday); + return 60 * (60 * (24L * x + t->tm_hour) + t->tm_min) + t->tm_sec; +} + +static time_t ff_time_to_epoch(uint16_t fdate, uint16_t ftime) { + struct tm tm; + memset(&tm, 0, sizeof(struct tm)); + tm.tm_sec = (ftime << 1) & 0x3e; + tm.tm_min = ((ftime >> 5) & 0x3f); + tm.tm_hour = ((ftime >> 11) & 0x1f); + tm.tm_mday = (fdate & 0x1f); + tm.tm_mon = ((fdate >> 5) & 0x0f) - 1; + tm.tm_year = ((fdate >> 9) & 0x7f) + 80; + return mg_timegm(&tm); +} + +static int ff_stat(const char *path, size_t *size, time_t *mtime) { + FILINFO fi; + if (path[0] == '\0') { + if (size) *size = 0; + if (mtime) *mtime = 0; + return MG_FS_DIR; + } else if (f_stat(path, &fi) == 0) { + if (size) *size = (size_t) fi.fsize; + if (mtime) *mtime = ff_time_to_epoch(fi.fdate, fi.ftime); + return MG_FS_READ | MG_FS_WRITE | ((fi.fattrib & AM_DIR) ? MG_FS_DIR : 0); + } else { + return 0; } - MG_ARM_ENABLE_IRQ(); - MG_DEBUG(("Flash write %lu bytes @ %p: %s. CR %#lx SR %#lx", len, dst, - ok ? "ok" : "fail", MG_REG(bank + FLASH_CR), - MG_REG(bank + FLASH_SR))); - MG_REG(bank + FLASH_CR) &= ~MG_BIT(1); // Clear programming flag - return ok; } -MG_IRAM void mg_device_reset(void) { - // SCB->AIRCR = ((0x5fa << SCB_AIRCR_VECTKEY_Pos)|SCB_AIRCR_SYSRESETREQ_Msk); - *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; +static void ff_list(const char *dir, void (*fn)(const char *, void *), + void *userdata) { + DIR d; + FILINFO fi; + if (f_opendir(&d, dir) == FR_OK) { + while (f_readdir(&d, &fi) == FR_OK && fi.fname[0] != '\0') { + if (!strcmp(fi.fname, ".") || !strcmp(fi.fname, "..")) continue; + fn(fi.fname, userdata); + } + f_closedir(&d); + } } -#endif -#ifdef MG_ENABLE_LINES -#line 1 "src/dns.c" -#endif +static void *ff_open(const char *path, int flags) { + FIL f; + unsigned char mode = FA_READ; + if (flags & MG_FS_WRITE) mode |= FA_WRITE | FA_OPEN_ALWAYS | FA_OPEN_APPEND; + if (f_open(&f, path, mode) == 0) { + FIL *fp; + if ((fp = calloc(1, sizeof(*fp))) != NULL) { + memcpy(fp, &f, sizeof(*fp)); + return fp; + } + } + return NULL; +} + +static void ff_close(void *fp) { + if (fp != NULL) { + f_close((FIL *) fp); + free(fp); + } +} + +static size_t ff_read(void *fp, void *buf, size_t len) { + UINT n = 0, misalign = ((size_t) buf) & 3; + if (misalign) { + char aligned[4]; + f_read((FIL *) fp, aligned, len > misalign ? misalign : len, &n); + memcpy(buf, aligned, n); + } else { + f_read((FIL *) fp, buf, len, &n); + } + return n; +} + +static size_t ff_write(void *fp, const void *buf, size_t len) { + UINT n = 0; + return f_write((FIL *) fp, (char *) buf, len, &n) == FR_OK ? n : 0; +} + +static size_t ff_seek(void *fp, size_t offset) { + f_lseek((FIL *) fp, offset); + return offset; +} + +static bool ff_rename(const char *from, const char *to) { + return f_rename(from, to) == FR_OK; +} +static bool ff_remove(const char *path) { + return f_unlink(path) == FR_OK; +} +static bool ff_mkdir(const char *path) { + return f_mkdir(path) == FR_OK; +} +struct mg_fs mg_fs_fat = {ff_stat, ff_list, ff_open, ff_close, ff_read, + ff_write, ff_seek, ff_rename, ff_remove, ff_mkdir}; +#endif +#ifdef MG_ENABLE_LINES +#line 1 "src/fs_packed.c" +#endif -struct dns_data { - struct dns_data *next; - struct mg_connection *c; - uint64_t expire; - uint16_t txnid; +struct packed_file { + const char *data; + size_t size; + size_t pos; }; -static void mg_sendnsreq(struct mg_connection *, struct mg_str *, int, - struct mg_dns *, bool); +#if MG_ENABLE_PACKED_FS +#else +const char *mg_unpack(const char *path, size_t *size, time_t *mtime) { + *size = 0, *mtime = 0; + (void) path; + return NULL; +} +const char *mg_unlist(size_t no) { + (void) no; + return NULL; +} +#endif -static void mg_dns_free(struct dns_data **head, struct dns_data *d) { - LIST_DELETE(struct dns_data, head, d); - free(d); +struct mg_str mg_unpacked(const char *path) { + size_t len = 0; + const char *buf = mg_unpack(path, &len, NULL); + return mg_str_n(buf, len); } -void mg_resolve_cancel(struct mg_connection *c) { - struct dns_data *tmp, *d; - struct dns_data **head = (struct dns_data **) &c->mgr->active_dns_requests; - for (d = *head; d != NULL; d = tmp) { - tmp = d->next; - if (d->c == c) mg_dns_free(head, d); +static int is_dir_prefix(const char *prefix, size_t n, const char *path) { + // MG_INFO(("[%.*s] [%s] %c", (int) n, prefix, path, path[n])); + return n < strlen(path) && strncmp(prefix, path, n) == 0 && + (n == 0 || path[n] == '/' || path[n - 1] == '/'); +} + +static int packed_stat(const char *path, size_t *size, time_t *mtime) { + const char *p; + size_t i, n = strlen(path); + if (mg_unpack(path, size, mtime)) return MG_FS_READ; // Regular file + // Scan all files. If `path` is a dir prefix for any of them, it's a dir + for (i = 0; (p = mg_unlist(i)) != NULL; i++) { + if (is_dir_prefix(path, n, p)) return MG_FS_DIR; } + return 0; } -static size_t mg_dns_parse_name_depth(const uint8_t *s, size_t len, size_t ofs, - char *to, size_t tolen, size_t j, - int depth) { - size_t i = 0; - if (tolen > 0 && depth == 0) to[0] = '\0'; - if (depth > 5) return 0; - // MG_INFO(("ofs %lx %x %x", (unsigned long) ofs, s[ofs], s[ofs + 1])); - while (ofs + i + 1 < len) { - size_t n = s[ofs + i]; - if (n == 0) { - i++; - break; - } - if (n & 0xc0) { - size_t ptr = (((n & 0x3f) << 8) | s[ofs + i + 1]); // 12 is hdr len - // MG_INFO(("PTR %lx", (unsigned long) ptr)); - if (ptr + 1 < len && (s[ptr] & 0xc0) == 0 && - mg_dns_parse_name_depth(s, len, ptr, to, tolen, j, depth + 1) == 0) - return 0; - i += 2; - break; - } - if (ofs + i + n + 1 >= len) return 0; - if (j > 0) { - if (j < tolen) to[j] = '.'; - j++; - } - if (j + n < tolen) memcpy(&to[j], &s[ofs + i + 1], n); - j += n; - i += n + 1; - if (j < tolen) to[j] = '\0'; // Zero-terminate this chunk - // MG_INFO(("--> [%s]", to)); +static void packed_list(const char *dir, void (*fn)(const char *, void *), + void *userdata) { + char buf[MG_PATH_MAX], tmp[sizeof(buf)]; + const char *path, *begin, *end; + size_t i, n = strlen(dir); + tmp[0] = '\0'; // Previously listed entry + for (i = 0; (path = mg_unlist(i)) != NULL; i++) { + if (!is_dir_prefix(dir, n, path)) continue; + begin = &path[n + 1]; + end = strchr(begin, '/'); + if (end == NULL) end = begin + strlen(begin); + mg_snprintf(buf, sizeof(buf), "%.*s", (int) (end - begin), begin); + buf[sizeof(buf) - 1] = '\0'; + // If this entry has been already listed, skip + // NOTE: we're assuming that file list is sorted alphabetically + if (strcmp(buf, tmp) == 0) continue; + fn(buf, userdata); // Not yet listed, call user function + strcpy(tmp, buf); // And save this entry as listed } - if (tolen > 0) to[tolen - 1] = '\0'; // Make sure make sure it is nul-term - return i; } -static size_t mg_dns_parse_name(const uint8_t *s, size_t n, size_t ofs, - char *dst, size_t dstlen) { - return mg_dns_parse_name_depth(s, n, ofs, dst, dstlen, 0, 0); +static void *packed_open(const char *path, int flags) { + size_t size = 0; + const char *data = mg_unpack(path, &size, NULL); + struct packed_file *fp = NULL; + if (data == NULL) return NULL; + if (flags & MG_FS_WRITE) return NULL; + if ((fp = (struct packed_file *) calloc(1, sizeof(*fp))) != NULL) { + fp->size = size; + fp->data = data; + } + return (void *) fp; } -size_t mg_dns_parse_rr(const uint8_t *buf, size_t len, size_t ofs, - bool is_question, struct mg_dns_rr *rr) { - const uint8_t *s = buf + ofs, *e = &buf[len]; +static void packed_close(void *fp) { + if (fp != NULL) free(fp); +} - memset(rr, 0, sizeof(*rr)); - if (len < sizeof(struct mg_dns_header)) return 0; // Too small - if (len > 512) return 0; // Too large, we don't expect that - if (s >= e) return 0; // Overflow +static size_t packed_read(void *fd, void *buf, size_t len) { + struct packed_file *fp = (struct packed_file *) fd; + if (fp->pos + len > fp->size) len = fp->size - fp->pos; + memcpy(buf, &fp->data[fp->pos], len); + fp->pos += len; + return len; +} - if ((rr->nlen = (uint16_t) mg_dns_parse_name(buf, len, ofs, NULL, 0)) == 0) - return 0; - s += rr->nlen + 4; - if (s > e) return 0; - rr->atype = (uint16_t) (((uint16_t) s[-4] << 8) | s[-3]); - rr->aclass = (uint16_t) (((uint16_t) s[-2] << 8) | s[-1]); - if (is_question) return (size_t) (rr->nlen + 4); - - s += 6; - if (s > e) return 0; - rr->alen = (uint16_t) (((uint16_t) s[-2] << 8) | s[-1]); - if (s + rr->alen > e) return 0; - return (size_t) (rr->nlen + rr->alen + 10); +static size_t packed_write(void *fd, const void *buf, size_t len) { + (void) fd, (void) buf, (void) len; + return 0; } -bool mg_dns_parse(const uint8_t *buf, size_t len, struct mg_dns_message *dm) { - const struct mg_dns_header *h = (struct mg_dns_header *) buf; - struct mg_dns_rr rr; - size_t i, n, num_answers, ofs = sizeof(*h); - memset(dm, 0, sizeof(*dm)); - - if (len < sizeof(*h)) return 0; // Too small, headers dont fit - if (mg_ntohs(h->num_questions) > 1) return 0; // Sanity - num_answers = mg_ntohs(h->num_answers); - if (num_answers > 10) { - MG_DEBUG(("Got %u answers, ignoring beyond 10th one", num_answers)); - num_answers = 10; // Sanity cap - } - dm->txnid = mg_ntohs(h->txnid); - - for (i = 0; i < mg_ntohs(h->num_questions); i++) { - if ((n = mg_dns_parse_rr(buf, len, ofs, true, &rr)) == 0) return false; - // MG_INFO(("Q %lu %lu %hu/%hu", ofs, n, rr.atype, rr.aclass)); - ofs += n; - } - for (i = 0; i < num_answers; i++) { - if ((n = mg_dns_parse_rr(buf, len, ofs, false, &rr)) == 0) return false; - // MG_INFO(("A -- %lu %lu %hu/%hu %s", ofs, n, rr.atype, rr.aclass, - // dm->name)); - mg_dns_parse_name(buf, len, ofs, dm->name, sizeof(dm->name)); - ofs += n; - - if (rr.alen == 4 && rr.atype == 1 && rr.aclass == 1) { - dm->addr.is_ip6 = false; - memcpy(&dm->addr.ip, &buf[ofs - 4], 4); - dm->resolved = true; - break; // Return success - } else if (rr.alen == 16 && rr.atype == 28 && rr.aclass == 1) { - dm->addr.is_ip6 = true; - memcpy(&dm->addr.ip, &buf[ofs - 16], 16); - dm->resolved = true; - break; // Return success - } - } - return true; +static size_t packed_seek(void *fd, size_t offset) { + struct packed_file *fp = (struct packed_file *) fd; + fp->pos = offset; + if (fp->pos > fp->size) fp->pos = fp->size; + return fp->pos; } -static void dns_cb(struct mg_connection *c, int ev, void *ev_data) { - struct dns_data *d, *tmp; - struct dns_data **head = (struct dns_data **) &c->mgr->active_dns_requests; - if (ev == MG_EV_POLL) { - uint64_t now = *(uint64_t *) ev_data; - for (d = *head; d != NULL; d = tmp) { - tmp = d->next; - // MG_DEBUG ("%lu %lu dns poll", d->expire, now)); - if (now > d->expire) mg_error(d->c, "DNS timeout"); - } - } else if (ev == MG_EV_READ) { - struct mg_dns_message dm; - int resolved = 0; - if (mg_dns_parse(c->recv.buf, c->recv.len, &dm) == false) { - MG_ERROR(("Unexpected DNS response:")); - mg_hexdump(c->recv.buf, c->recv.len); - } else { - // MG_VERBOSE(("%s %d", dm.name, dm.resolved)); - for (d = *head; d != NULL; d = tmp) { - tmp = d->next; - // MG_INFO(("d %p %hu %hu", d, d->txnid, dm.txnid)); - if (dm.txnid != d->txnid) continue; - if (d->c->is_resolving) { - if (dm.resolved) { - dm.addr.port = d->c->rem.port; // Save port - d->c->rem = dm.addr; // Copy resolved address - MG_DEBUG( - ("%lu %s is %M", d->c->id, dm.name, mg_print_ip, &d->c->rem)); - mg_connect_resolved(d->c); -#if MG_ENABLE_IPV6 - } else if (dm.addr.is_ip6 == false && dm.name[0] != '\0' && - c->mgr->use_dns6 == false) { - struct mg_str x = mg_str(dm.name); - mg_sendnsreq(d->c, &x, c->mgr->dnstimeout, &c->mgr->dns6, true); -#endif - } else { - mg_error(d->c, "%s DNS lookup failed", dm.name); - } - } else { - MG_ERROR(("%lu already resolved", d->c->id)); - } - mg_dns_free(head, d); - resolved = 1; - } - } - if (!resolved) MG_ERROR(("stray DNS reply")); - c->recv.len = 0; - } else if (ev == MG_EV_CLOSE) { - for (d = *head; d != NULL; d = tmp) { - tmp = d->next; - mg_error(d->c, "DNS error"); - mg_dns_free(head, d); - } - } +static bool packed_rename(const char *from, const char *to) { + (void) from, (void) to; + return false; } -static bool mg_dns_send(struct mg_connection *c, const struct mg_str *name, - uint16_t txnid, bool ipv6) { - struct { - struct mg_dns_header header; - uint8_t data[256]; - } pkt; - size_t i, n; - memset(&pkt, 0, sizeof(pkt)); - pkt.header.txnid = mg_htons(txnid); - pkt.header.flags = mg_htons(0x100); - pkt.header.num_questions = mg_htons(1); - for (i = n = 0; i < sizeof(pkt.data) - 5; i++) { - if (name->buf[i] == '.' || i >= name->len) { - pkt.data[n] = (uint8_t) (i - n); - memcpy(&pkt.data[n + 1], name->buf + n, i - n); - n = i + 1; - } - if (i >= name->len) break; - } - memcpy(&pkt.data[n], "\x00\x00\x01\x00\x01", 5); // A query - n += 5; - if (ipv6) pkt.data[n - 3] = 0x1c; // AAAA query - // memcpy(&pkt.data[n], "\xc0\x0c\x00\x1c\x00\x01", 6); // AAAA query - // n += 6; - return mg_send(c, &pkt, sizeof(pkt.header) + n); +static bool packed_remove(const char *path) { + (void) path; + return false; } -static void mg_sendnsreq(struct mg_connection *c, struct mg_str *name, int ms, - struct mg_dns *dnsc, bool ipv6) { - struct dns_data *d = NULL; - if (dnsc->url == NULL) { - mg_error(c, "DNS server URL is NULL. Call mg_mgr_init()"); - } else if (dnsc->c == NULL) { - dnsc->c = mg_connect(c->mgr, dnsc->url, NULL, NULL); - if (dnsc->c != NULL) { - dnsc->c->pfn = dns_cb; - // dnsc->c->is_hexdumping = 1; - } - } - if (dnsc->c == NULL) { - mg_error(c, "resolver"); - } else if ((d = (struct dns_data *) calloc(1, sizeof(*d))) == NULL) { - mg_error(c, "resolve OOM"); - } else { - struct dns_data *reqs = (struct dns_data *) c->mgr->active_dns_requests; - d->txnid = reqs ? (uint16_t) (reqs->txnid + 1) : 1; - d->next = (struct dns_data *) c->mgr->active_dns_requests; - c->mgr->active_dns_requests = d; - d->expire = mg_millis() + (uint64_t) ms; - d->c = c; - c->is_resolving = 1; - MG_VERBOSE(("%lu resolving %.*s @ %s, txnid %hu", c->id, (int) name->len, - name->buf, dnsc->url, d->txnid)); - if (!mg_dns_send(dnsc->c, name, d->txnid, ipv6)) { - mg_error(dnsc->c, "DNS send"); - } - } +static bool packed_mkdir(const char *path) { + (void) path; + return false; } -void mg_resolve(struct mg_connection *c, const char *url) { - struct mg_str host = mg_url_host(url); - c->rem.port = mg_htons(mg_url_port(url)); - if (mg_aton(host, &c->rem)) { - // host is an IP address, do not fire name resolution - mg_connect_resolved(c); - } else { - // host is not an IP, send DNS resolution request - struct mg_dns *dns = c->mgr->use_dns6 ? &c->mgr->dns6 : &c->mgr->dns4; - mg_sendnsreq(c, &host, c->mgr->dnstimeout, dns, c->mgr->use_dns6); - } -} +struct mg_fs mg_fs_packed = { + packed_stat, packed_list, packed_open, packed_close, packed_read, + packed_write, packed_seek, packed_rename, packed_remove, packed_mkdir}; #ifdef MG_ENABLE_LINES -#line 1 "src/event.c" +#line 1 "src/fs_posix.c" #endif +#if MG_ENABLE_POSIX_FS +#ifndef MG_STAT_STRUCT +#define MG_STAT_STRUCT stat +#endif +#ifndef MG_STAT_FUNC +#define MG_STAT_FUNC stat +#endif - -void mg_call(struct mg_connection *c, int ev, void *ev_data) { -#if MG_ENABLE_PROFILE - const char *names[] = { - "EV_ERROR", "EV_OPEN", "EV_POLL", "EV_RESOLVE", - "EV_CONNECT", "EV_ACCEPT", "EV_TLS_HS", "EV_READ", - "EV_WRITE", "EV_CLOSE", "EV_HTTP_MSG", "EV_HTTP_CHUNK", - "EV_WS_OPEN", "EV_WS_MSG", "EV_WS_CTL", "EV_MQTT_CMD", - "EV_MQTT_MSG", "EV_MQTT_OPEN", "EV_SNTP_TIME", "EV_USER"}; - if (ev != MG_EV_POLL && ev < (int) (sizeof(names) / sizeof(names[0]))) { - MG_PROF_ADD(c, names[ev]); +static int p_stat(const char *path, size_t *size, time_t *mtime) { +#if !defined(S_ISDIR) + MG_ERROR(("stat() API is not supported. %p %p %p", path, size, mtime)); + return 0; +#else +#if MG_ARCH == MG_ARCH_WIN32 + struct _stati64 st; + wchar_t tmp[MG_PATH_MAX]; + MultiByteToWideChar(CP_UTF8, 0, path, -1, tmp, sizeof(tmp) / sizeof(tmp[0])); + if (_wstati64(tmp, &st) != 0) return 0; + // If path is a symlink, windows reports 0 in st.st_size. + // Get a real file size by opening it and jumping to the end + if (st.st_size == 0 && (st.st_mode & _S_IFREG)) { + FILE *fp = _wfopen(tmp, L"rb"); + if (fp != NULL) { + fseek(fp, 0, SEEK_END); + if (ftell(fp) > 0) st.st_size = ftell(fp); // Use _ftelli64 on win10+ + fclose(fp); + } } +#else + struct MG_STAT_STRUCT st; + if (MG_STAT_FUNC(path, &st) != 0) return 0; +#endif + if (size) *size = (size_t) st.st_size; + if (mtime) *mtime = st.st_mtime; + return MG_FS_READ | MG_FS_WRITE | (S_ISDIR(st.st_mode) ? MG_FS_DIR : 0); #endif - // Fire protocol handler first, user handler second. See #2559 - if (c->pfn != NULL) c->pfn(c, ev, ev_data); - if (c->fn != NULL) c->fn(c, ev, ev_data); -} - -void mg_error(struct mg_connection *c, const char *fmt, ...) { - char buf[64]; - va_list ap; - va_start(ap, fmt); - mg_vsnprintf(buf, sizeof(buf), fmt, &ap); - va_end(ap); - MG_ERROR(("%lu %ld %s", c->id, c->fd, buf)); - c->is_closing = 1; // Set is_closing before sending MG_EV_CALL - mg_call(c, MG_EV_ERROR, buf); // Let user handler override it } -#ifdef MG_ENABLE_LINES -#line 1 "src/fmt.c" -#endif - +#if MG_ARCH == MG_ARCH_WIN32 +struct dirent { + char d_name[MAX_PATH]; +}; +typedef struct win32_dir { + HANDLE handle; + WIN32_FIND_DATAW info; + struct dirent result; +} DIR; +#if 0 +int gettimeofday(struct timeval *tv, void *tz) { + FILETIME ft; + unsigned __int64 tmpres = 0; -static bool is_digit(int c) { - return c >= '0' && c <= '9'; + if (tv != NULL) { + GetSystemTimeAsFileTime(&ft); + tmpres |= ft.dwHighDateTime; + tmpres <<= 32; + tmpres |= ft.dwLowDateTime; + tmpres /= 10; // convert into microseconds + tmpres -= (int64_t) 11644473600000000; + tv->tv_sec = (long) (tmpres / 1000000UL); + tv->tv_usec = (long) (tmpres % 1000000UL); + } + (void) tz; + return 0; } +#endif -static int addexp(char *buf, int e, int sign) { - int n = 0; - buf[n++] = 'e'; - buf[n++] = (char) sign; - if (e > 400) return 0; - if (e < 10) buf[n++] = '0'; - if (e >= 100) buf[n++] = (char) (e / 100 + '0'), e -= 100 * (e / 100); - if (e >= 10) buf[n++] = (char) (e / 10 + '0'), e -= 10 * (e / 10); - buf[n++] = (char) (e + '0'); - return n; +static int to_wchar(const char *path, wchar_t *wbuf, size_t wbuf_len) { + int ret; + char buf[MAX_PATH * 2], buf2[MAX_PATH * 2], *p; + strncpy(buf, path, sizeof(buf)); + buf[sizeof(buf) - 1] = '\0'; + // Trim trailing slashes. Leave backslash for paths like "X:\" + p = buf + strlen(buf) - 1; + while (p > buf && p[-1] != ':' && (p[0] == '\\' || p[0] == '/')) *p-- = '\0'; + memset(wbuf, 0, wbuf_len * sizeof(wchar_t)); + ret = MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, (int) wbuf_len); + // Convert back to Unicode. If doubly-converted string does not match the + // original, something is fishy, reject. + WideCharToMultiByte(CP_UTF8, 0, wbuf, (int) wbuf_len, buf2, sizeof(buf2), + NULL, NULL); + if (strcmp(buf, buf2) != 0) { + wbuf[0] = L'\0'; + ret = 0; + } + return ret; } -static int xisinf(double x) { - union { - double f; - uint64_t u; - } ieee754 = {x}; - return ((unsigned) (ieee754.u >> 32) & 0x7fffffff) == 0x7ff00000 && - ((unsigned) ieee754.u == 0); -} +DIR *opendir(const char *name) { + DIR *d = NULL; + wchar_t wpath[MAX_PATH]; + DWORD attrs; -static int xisnan(double x) { - union { - double f; - uint64_t u; - } ieee754 = {x}; - return ((unsigned) (ieee754.u >> 32) & 0x7fffffff) + - ((unsigned) ieee754.u != 0) > - 0x7ff00000; + if (name == NULL) { + SetLastError(ERROR_BAD_ARGUMENTS); + } else if ((d = (DIR *) calloc(1, sizeof(*d))) == NULL) { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + } else { + to_wchar(name, wpath, sizeof(wpath) / sizeof(wpath[0])); + attrs = GetFileAttributesW(wpath); + if (attrs != 0Xffffffff && (attrs & FILE_ATTRIBUTE_DIRECTORY)) { + (void) wcscat(wpath, L"\\*"); + d->handle = FindFirstFileW(wpath, &d->info); + d->result.d_name[0] = '\0'; + } else { + free(d); + d = NULL; + } + } + return d; } -static size_t mg_dtoa(char *dst, size_t dstlen, double d, int width, bool tz) { - char buf[40]; - int i, s = 0, n = 0, e = 0; - double t, mul, saved; - if (d == 0.0) return mg_snprintf(dst, dstlen, "%s", "0"); - if (xisinf(d)) return mg_snprintf(dst, dstlen, "%s", d > 0 ? "inf" : "-inf"); - if (xisnan(d)) return mg_snprintf(dst, dstlen, "%s", "nan"); - if (d < 0.0) d = -d, buf[s++] = '-'; - - // Round - saved = d; - mul = 1.0; - while (d >= 10.0 && d / mul >= 10.0) mul *= 10.0; - while (d <= 1.0 && d / mul <= 1.0) mul /= 10.0; - for (i = 0, t = mul * 5; i < width; i++) t /= 10.0; - d += t; - // Calculate exponent, and 'mul' for scientific representation - mul = 1.0; - while (d >= 10.0 && d / mul >= 10.0) mul *= 10.0, e++; - while (d < 1.0 && d / mul < 1.0) mul /= 10.0, e--; - // printf(" --> %g %d %g %g\n", saved, e, t, mul); - - if (e >= width && width > 1) { - n = (int) mg_dtoa(buf, sizeof(buf), saved / mul, width, tz); - // printf(" --> %.*g %d [%.*s]\n", 10, d / t, e, n, buf); - n += addexp(buf + s + n, e, '+'); - return mg_snprintf(dst, dstlen, "%.*s", n, buf); - } else if (e <= -width && width > 1) { - n = (int) mg_dtoa(buf, sizeof(buf), saved / mul, width, tz); - // printf(" --> %.*g %d [%.*s]\n", 10, d / mul, e, n, buf); - n += addexp(buf + s + n, -e, '-'); - return mg_snprintf(dst, dstlen, "%.*s", n, buf); +int closedir(DIR *d) { + int result = 0; + if (d != NULL) { + if (d->handle != INVALID_HANDLE_VALUE) + result = FindClose(d->handle) ? 0 : -1; + free(d); } else { - for (i = 0, t = mul; t >= 1.0 && s + n < (int) sizeof(buf); i++) { - int ch = (int) (d / t); - if (n > 0 || ch > 0) buf[s + n++] = (char) (ch + '0'); - d -= ch * t; - t /= 10.0; - } - // printf(" --> [%g] -> %g %g (%d) [%.*s]\n", saved, d, t, n, s + n, buf); - if (n == 0) buf[s++] = '0'; - while (t >= 1.0 && n + s < (int) sizeof(buf)) buf[n++] = '0', t /= 10.0; - if (s + n < (int) sizeof(buf)) buf[n + s++] = '.'; - // printf(" 1--> [%g] -> [%.*s]\n", saved, s + n, buf); - for (i = 0, t = 0.1; s + n < (int) sizeof(buf) && n < width; i++) { - int ch = (int) (d / t); - buf[s + n++] = (char) (ch + '0'); - d -= ch * t; - t /= 10.0; - } + result = -1; + SetLastError(ERROR_BAD_ARGUMENTS); } - while (tz && n > 0 && buf[s + n - 1] == '0') n--; // Trim trailing zeroes - if (n > 0 && buf[s + n - 1] == '.') n--; // Trim trailing dot - n += s; - if (n >= (int) sizeof(buf)) n = (int) sizeof(buf) - 1; - buf[n] = '\0'; - return mg_snprintf(dst, dstlen, "%s", buf); + return result; } -static size_t mg_lld(char *buf, int64_t val, bool is_signed, bool is_hex) { - const char *letters = "0123456789abcdef"; - uint64_t v = (uint64_t) val; - size_t s = 0, n, i; - if (is_signed && val < 0) buf[s++] = '-', v = (uint64_t) (-val); - // This loop prints a number in reverse order. I guess this is because we - // write numbers from right to left: least significant digit comes last. - // Maybe because we use Arabic numbers, and Arabs write RTL? - if (is_hex) { - for (n = 0; v; v >>= 4) buf[s + n++] = letters[v & 15]; +struct dirent *readdir(DIR *d) { + struct dirent *result = NULL; + if (d != NULL) { + memset(&d->result, 0, sizeof(d->result)); + if (d->handle != INVALID_HANDLE_VALUE) { + result = &d->result; + WideCharToMultiByte(CP_UTF8, 0, d->info.cFileName, -1, result->d_name, + sizeof(result->d_name), NULL, NULL); + if (!FindNextFileW(d->handle, &d->info)) { + FindClose(d->handle); + d->handle = INVALID_HANDLE_VALUE; + } + } else { + SetLastError(ERROR_FILE_NOT_FOUND); + } } else { - for (n = 0; v; v /= 10) buf[s + n++] = letters[v % 10]; + SetLastError(ERROR_BAD_ARGUMENTS); } - // Reverse a string - for (i = 0; i < n / 2; i++) { - char t = buf[s + i]; - buf[s + i] = buf[s + n - i - 1], buf[s + n - i - 1] = t; + return result; +} +#endif + +static void p_list(const char *dir, void (*fn)(const char *, void *), + void *userdata) { +#if MG_ENABLE_DIRLIST + struct dirent *dp; + DIR *dirp; + if ((dirp = (opendir(dir))) == NULL) return; + while ((dp = readdir(dirp)) != NULL) { + if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) continue; + fn(dp->d_name, userdata); } - if (val == 0) buf[n++] = '0'; // Handle special case - return n + s; + closedir(dirp); +#else + (void) dir, (void) fn, (void) userdata; +#endif } -static size_t scpy(void (*out)(char, void *), void *ptr, char *buf, - size_t len) { - size_t i = 0; - while (i < len && buf[i] != '\0') out(buf[i++], ptr); - return i; +static void *p_open(const char *path, int flags) { +#if MG_ARCH == MG_ARCH_WIN32 + const char *mode = flags == MG_FS_READ ? "rb" : "a+b"; + wchar_t b1[MG_PATH_MAX], b2[10]; + MultiByteToWideChar(CP_UTF8, 0, path, -1, b1, sizeof(b1) / sizeof(b1[0])); + MultiByteToWideChar(CP_UTF8, 0, mode, -1, b2, sizeof(b2) / sizeof(b2[0])); + return (void *) _wfopen(b1, b2); +#else + const char *mode = flags == MG_FS_READ ? "rbe" : "a+be"; // e for CLOEXEC + return (void *) fopen(path, mode); +#endif } -size_t mg_xprintf(void (*out)(char, void *), void *ptr, const char *fmt, ...) { - size_t len = 0; - va_list ap; - va_start(ap, fmt); - len = mg_vxprintf(out, ptr, fmt, &ap); - va_end(ap); - return len; +static void p_close(void *fp) { + fclose((FILE *) fp); } -size_t mg_vxprintf(void (*out)(char, void *), void *param, const char *fmt, - va_list *ap) { - size_t i = 0, n = 0; - while (fmt[i] != '\0') { - if (fmt[i] == '%') { - size_t j, k, x = 0, is_long = 0, w = 0 /* width */, pr = ~0U /* prec */; - char pad = ' ', minus = 0, c = fmt[++i]; - if (c == '#') x++, c = fmt[++i]; - if (c == '-') minus++, c = fmt[++i]; - if (c == '0') pad = '0', c = fmt[++i]; - while (is_digit(c)) w *= 10, w += (size_t) (c - '0'), c = fmt[++i]; - if (c == '.') { - c = fmt[++i]; - if (c == '*') { - pr = (size_t) va_arg(*ap, int); - c = fmt[++i]; - } else { - pr = 0; - while (is_digit(c)) pr *= 10, pr += (size_t) (c - '0'), c = fmt[++i]; - } - } - while (c == 'h') c = fmt[++i]; // Treat h and hh as int - if (c == 'l') { - is_long++, c = fmt[++i]; - if (c == 'l') is_long++, c = fmt[++i]; - } - if (c == 'p') x = 1, is_long = 1; - if (c == 'd' || c == 'u' || c == 'x' || c == 'X' || c == 'p' || - c == 'g' || c == 'f') { - bool s = (c == 'd'), h = (c == 'x' || c == 'X' || c == 'p'); - char tmp[40]; - size_t xl = x ? 2 : 0; - if (c == 'g' || c == 'f') { - double v = va_arg(*ap, double); - if (pr == ~0U) pr = 6; - k = mg_dtoa(tmp, sizeof(tmp), v, (int) pr, c == 'g'); - } else if (is_long == 2) { - int64_t v = va_arg(*ap, int64_t); - k = mg_lld(tmp, v, s, h); - } else if (is_long == 1) { - long v = va_arg(*ap, long); - k = mg_lld(tmp, s ? (int64_t) v : (int64_t) (unsigned long) v, s, h); - } else { - int v = va_arg(*ap, int); - k = mg_lld(tmp, s ? (int64_t) v : (int64_t) (unsigned) v, s, h); - } - for (j = 0; j < xl && w > 0; j++) w--; - for (j = 0; pad == ' ' && !minus && k < w && j + k < w; j++) - n += scpy(out, param, &pad, 1); - n += scpy(out, param, (char *) "0x", xl); - for (j = 0; pad == '0' && k < w && j + k < w; j++) - n += scpy(out, param, &pad, 1); - n += scpy(out, param, tmp, k); - for (j = 0; pad == ' ' && minus && k < w && j + k < w; j++) - n += scpy(out, param, &pad, 1); - } else if (c == 'm' || c == 'M') { - mg_pm_t f = va_arg(*ap, mg_pm_t); - if (c == 'm') out('"', param); - n += f(out, param, ap); - if (c == 'm') n += 2, out('"', param); - } else if (c == 'c') { - int ch = va_arg(*ap, int); - out((char) ch, param); - n++; - } else if (c == 's') { - char *p = va_arg(*ap, char *); - if (pr == ~0U) pr = p == NULL ? 0 : strlen(p); - for (j = 0; !minus && pr < w && j + pr < w; j++) - n += scpy(out, param, &pad, 1); - n += scpy(out, param, p, pr); - for (j = 0; minus && pr < w && j + pr < w; j++) - n += scpy(out, param, &pad, 1); - } else if (c == '%') { - out('%', param); - n++; - } else { - out('%', param); - out(c, param); - n += 2; - } - i++; - } else { - out(fmt[i], param), n++, i++; - } - } - return n; +static size_t p_read(void *fp, void *buf, size_t len) { + return fread(buf, 1, len, (FILE *) fp); } -#ifdef MG_ENABLE_LINES -#line 1 "src/fs.c" -#endif - - - - -struct mg_fd *mg_fs_open(struct mg_fs *fs, const char *path, int flags) { - struct mg_fd *fd = (struct mg_fd *) calloc(1, sizeof(*fd)); - if (fd != NULL) { - fd->fd = fs->op(path, flags); - fd->fs = fs; - if (fd->fd == NULL) { - free(fd); - fd = NULL; - } - } - return fd; +static size_t p_write(void *fp, const void *buf, size_t len) { + return fwrite(buf, 1, len, (FILE *) fp); } -void mg_fs_close(struct mg_fd *fd) { - if (fd != NULL) { - fd->fs->cl(fd->fd); - free(fd); - } +static size_t p_seek(void *fp, size_t offset) { +#if (defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64) || \ + (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 200112L) || \ + (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 600) + if (fseeko((FILE *) fp, (off_t) offset, SEEK_SET) != 0) (void) 0; +#else + if (fseek((FILE *) fp, (long) offset, SEEK_SET) != 0) (void) 0; +#endif + return (size_t) ftell((FILE *) fp); } -struct mg_str mg_file_read(struct mg_fs *fs, const char *path) { - struct mg_str result = {NULL, 0}; - void *fp; - fs->st(path, &result.len, NULL); - if ((fp = fs->op(path, MG_FS_READ)) != NULL) { - result.buf = (char *) calloc(1, result.len + 1); - if (result.buf != NULL && - fs->rd(fp, (void *) result.buf, result.len) != result.len) { - free((void *) result.buf); - result.buf = NULL; - } - fs->cl(fp); - } - if (result.buf == NULL) result.len = 0; - return result; +static bool p_rename(const char *from, const char *to) { + return rename(from, to) == 0; } -bool mg_file_write(struct mg_fs *fs, const char *path, const void *buf, - size_t len) { - bool result = false; - struct mg_fd *fd; - char tmp[MG_PATH_MAX]; - mg_snprintf(tmp, sizeof(tmp), "%s..%d", path, rand()); - if ((fd = mg_fs_open(fs, tmp, MG_FS_WRITE)) != NULL) { - result = fs->wr(fd->fd, buf, len) == len; - mg_fs_close(fd); - if (result) { - fs->rm(path); - fs->mv(tmp, path); - } else { - fs->rm(tmp); - } - } - return result; +static bool p_remove(const char *path) { + return remove(path) == 0; } -bool mg_file_printf(struct mg_fs *fs, const char *path, const char *fmt, ...) { - va_list ap; - char *data; - bool result = false; - va_start(ap, fmt); - data = mg_vmprintf(fmt, &ap); - va_end(ap); - result = mg_file_write(fs, path, data, strlen(data)); - free(data); - return result; +static bool p_mkdir(const char *path) { + return mkdir(path, 0775) == 0; } -// This helper function allows to scan a filesystem in a sequential way, -// without using callback function: -// char buf[100] = ""; -// while (mg_fs_ls(&mg_fs_posix, "./", buf, sizeof(buf))) { -// ... -static void mg_fs_ls_fn(const char *filename, void *param) { - struct mg_str *s = (struct mg_str *) param; - if (s->buf[0] == '\0') { - mg_snprintf((char *) s->buf, s->len, "%s", filename); - } else if (strcmp(s->buf, filename) == 0) { - ((char *) s->buf)[0] = '\0'; // Fetch next file - } -} +#else -bool mg_fs_ls(struct mg_fs *fs, const char *path, char *buf, size_t len) { - struct mg_str s = {buf, len}; - fs->ls(path, mg_fs_ls_fn, &s); - return buf[0] != '\0'; +static int p_stat(const char *path, size_t *size, time_t *mtime) { + (void) path, (void) size, (void) mtime; + return 0; +} +static void p_list(const char *path, void (*fn)(const char *, void *), + void *userdata) { + (void) path, (void) fn, (void) userdata; +} +static void *p_open(const char *path, int flags) { + (void) path, (void) flags; + return NULL; +} +static void p_close(void *fp) { + (void) fp; +} +static size_t p_read(void *fd, void *buf, size_t len) { + (void) fd, (void) buf, (void) len; + return 0; +} +static size_t p_write(void *fd, const void *buf, size_t len) { + (void) fd, (void) buf, (void) len; + return 0; +} +static size_t p_seek(void *fd, size_t offset) { + (void) fd, (void) offset; + return (size_t) ~0; +} +static bool p_rename(const char *from, const char *to) { + (void) from, (void) to; + return false; } +static bool p_remove(const char *path) { + (void) path; + return false; +} +static bool p_mkdir(const char *path) { + (void) path; + return false; +} +#endif + +struct mg_fs mg_fs_posix = {p_stat, p_list, p_open, p_close, p_read, + p_write, p_seek, p_rename, p_remove, p_mkdir}; #ifdef MG_ENABLE_LINES -#line 1 "src/fs_fat.c" +#line 1 "src/http.c" #endif -#if MG_ENABLE_FATFS -#include -static int mg_days_from_epoch(int y, int m, int d) { - y -= m <= 2; - int era = y / 400; - int yoe = y - era * 400; - int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; - int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - return era * 146097 + doe - 719468; -} -static time_t mg_timegm(const struct tm *t) { - int year = t->tm_year + 1900; - int month = t->tm_mon; // 0-11 - if (month > 11) { - year += month / 12; - month %= 12; - } else if (month < 0) { - int years_diff = (11 - month) / 12; - year -= years_diff; - month += 12 * years_diff; - } - int x = mg_days_from_epoch(year, month + 1, t->tm_mday); - return 60 * (60 * (24L * x + t->tm_hour) + t->tm_min) + t->tm_sec; -} -static time_t ff_time_to_epoch(uint16_t fdate, uint16_t ftime) { - struct tm tm; - memset(&tm, 0, sizeof(struct tm)); - tm.tm_sec = (ftime << 1) & 0x3e; - tm.tm_min = ((ftime >> 5) & 0x3f); - tm.tm_hour = ((ftime >> 11) & 0x1f); - tm.tm_mday = (fdate & 0x1f); - tm.tm_mon = ((fdate >> 5) & 0x0f) - 1; - tm.tm_year = ((fdate >> 9) & 0x7f) + 80; - return mg_timegm(&tm); -} -static int ff_stat(const char *path, size_t *size, time_t *mtime) { - FILINFO fi; - if (path[0] == '\0') { - if (size) *size = 0; - if (mtime) *mtime = 0; - return MG_FS_DIR; - } else if (f_stat(path, &fi) == 0) { - if (size) *size = (size_t) fi.fsize; - if (mtime) *mtime = ff_time_to_epoch(fi.fdate, fi.ftime); - return MG_FS_READ | MG_FS_WRITE | ((fi.fattrib & AM_DIR) ? MG_FS_DIR : 0); - } else { - return 0; - } -} -static void ff_list(const char *dir, void (*fn)(const char *, void *), - void *userdata) { - DIR d; - FILINFO fi; - if (f_opendir(&d, dir) == FR_OK) { - while (f_readdir(&d, &fi) == FR_OK && fi.fname[0] != '\0') { - if (!strcmp(fi.fname, ".") || !strcmp(fi.fname, "..")) continue; - fn(fi.fname, userdata); - } - f_closedir(&d); - } + + + + + +static int mg_ncasecmp(const char *s1, const char *s2, size_t len) { + int diff = 0; + if (len > 0) do { + int c = *s1++, d = *s2++; + if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + if (d >= 'A' && d <= 'Z') d += 'a' - 'A'; + diff = c - d; + } while (diff == 0 && s1[-1] != '\0' && --len > 0); + return diff; } -static void *ff_open(const char *path, int flags) { - FIL f; - unsigned char mode = FA_READ; - if (flags & MG_FS_WRITE) mode |= FA_WRITE | FA_OPEN_ALWAYS | FA_OPEN_APPEND; - if (f_open(&f, path, mode) == 0) { - FIL *fp; - if ((fp = calloc(1, sizeof(*fp))) != NULL) { - memcpy(fp, &f, sizeof(*fp)); - return fp; - } +bool mg_to_size_t(struct mg_str str, size_t *val); +bool mg_to_size_t(struct mg_str str, size_t *val) { + size_t i = 0, max = (size_t) -1, max2 = max / 10, result = 0, ndigits = 0; + while (i < str.len && (str.buf[i] == ' ' || str.buf[i] == '\t')) i++; + if (i < str.len && str.buf[i] == '-') return false; + while (i < str.len && str.buf[i] >= '0' && str.buf[i] <= '9') { + size_t digit = (size_t) (str.buf[i] - '0'); + if (result > max2) return false; // Overflow + result *= 10; + if (result > max - digit) return false; // Overflow + result += digit; + i++, ndigits++; } - return NULL; + while (i < str.len && (str.buf[i] == ' ' || str.buf[i] == '\t')) i++; + if (ndigits == 0) return false; // #2322: Content-Length = 1 * DIGIT + if (i != str.len) return false; // Ditto + *val = (size_t) result; + return true; } -static void ff_close(void *fp) { - if (fp != NULL) { - f_close((FIL *) fp); - free(fp); +// Chunk deletion marker is the MSB in the "processed" counter +#define MG_DMARK ((size_t) 1 << (sizeof(size_t) * 8 - 1)) + +// Multipart POST example: +// --xyz +// Content-Disposition: form-data; name="val" +// +// abcdef +// --xyz +// Content-Disposition: form-data; name="foo"; filename="a.txt" +// Content-Type: text/plain +// +// hello world +// +// --xyz-- +size_t mg_http_next_multipart(struct mg_str body, size_t ofs, + struct mg_http_part *part) { + struct mg_str cd = mg_str_n("Content-Disposition", 19); + const char *s = body.buf; + size_t b = ofs, h1, h2, b1, b2, max = body.len; + + // Init part params + if (part != NULL) part->name = part->filename = part->body = mg_str_n(0, 0); + + // Skip boundary + while (b + 2 < max && s[b] != '\r' && s[b + 1] != '\n') b++; + if (b <= ofs || b + 2 >= max) return 0; + // MG_INFO(("B: %zu %zu [%.*s]", ofs, b - ofs, (int) (b - ofs), s)); + + // Skip headers + h1 = h2 = b + 2; + for (;;) { + while (h2 + 2 < max && s[h2] != '\r' && s[h2 + 1] != '\n') h2++; + if (h2 == h1) break; + if (h2 + 2 >= max) return 0; + // MG_INFO(("Header: [%.*s]", (int) (h2 - h1), &s[h1])); + if (part != NULL && h1 + cd.len + 2 < h2 && s[h1 + cd.len] == ':' && + mg_ncasecmp(&s[h1], cd.buf, cd.len) == 0) { + struct mg_str v = mg_str_n(&s[h1 + cd.len + 2], h2 - (h1 + cd.len + 2)); + part->name = mg_http_get_header_var(v, mg_str_n("name", 4)); + part->filename = mg_http_get_header_var(v, mg_str_n("filename", 8)); + } + h1 = h2 = h2 + 2; } + b1 = b2 = h2 + 2; + while (b2 + 2 + (b - ofs) + 2 < max && !(s[b2] == '\r' && s[b2 + 1] == '\n' && + memcmp(&s[b2 + 2], s, b - ofs) == 0)) + b2++; + + if (b2 + 2 >= max) return 0; + if (part != NULL) part->body = mg_str_n(&s[b1], b2 - b1); + // MG_INFO(("Body: [%.*s]", (int) (b2 - b1), &s[b1])); + return b2 + 2; } -static size_t ff_read(void *fp, void *buf, size_t len) { - UINT n = 0, misalign = ((size_t) buf) & 3; - if (misalign) { - char aligned[4]; - f_read((FIL *) fp, aligned, len > misalign ? misalign : len, &n); - memcpy(buf, aligned, n); +void mg_http_bauth(struct mg_connection *c, const char *user, + const char *pass) { + struct mg_str u = mg_str(user), p = mg_str(pass); + size_t need = c->send.len + 36 + (u.len + p.len) * 2; + if (c->send.size < need) mg_iobuf_resize(&c->send, need); + if (c->send.size >= need) { + size_t i, n = 0; + char *buf = (char *) &c->send.buf[c->send.len]; + memcpy(buf, "Authorization: Basic ", 21); // DON'T use mg_send! + for (i = 0; i < u.len; i++) { + n = mg_base64_update(((unsigned char *) u.buf)[i], buf + 21, n); + } + if (p.len > 0) { + n = mg_base64_update(':', buf + 21, n); + for (i = 0; i < p.len; i++) { + n = mg_base64_update(((unsigned char *) p.buf)[i], buf + 21, n); + } + } + n = mg_base64_final(buf + 21, n); + c->send.len += 21 + (size_t) n + 2; + memcpy(&c->send.buf[c->send.len - 2], "\r\n", 2); } else { - f_read((FIL *) fp, buf, len, &n); + MG_ERROR(("%lu oom %d->%d ", c->id, (int) c->send.size, (int) need)); } - return n; } -static size_t ff_write(void *fp, const void *buf, size_t len) { - UINT n = 0; - return f_write((FIL *) fp, (char *) buf, len, &n) == FR_OK ? n : 0; +struct mg_str mg_http_var(struct mg_str buf, struct mg_str name) { + struct mg_str entry, k, v, result = mg_str_n(NULL, 0); + while (mg_span(buf, &entry, &buf, '&')) { + if (mg_span(entry, &k, &v, '=') && name.len == k.len && + mg_ncasecmp(name.buf, k.buf, k.len) == 0) { + result = v; + break; + } + } + return result; } -static size_t ff_seek(void *fp, size_t offset) { - f_lseek((FIL *) fp, offset); - return offset; +int mg_http_get_var(const struct mg_str *buf, const char *name, char *dst, + size_t dst_len) { + int len; + if (dst != NULL && dst_len > 0) { + dst[0] = '\0'; // If destination buffer is valid, always nul-terminate it + } + if (dst == NULL || dst_len == 0) { + len = -2; // Bad destination + } else if (buf->buf == NULL || name == NULL || buf->len == 0) { + len = -1; // Bad source + } else { + struct mg_str v = mg_http_var(*buf, mg_str(name)); + if (v.buf == NULL) { + len = -4; // Name does not exist + } else { + len = mg_url_decode(v.buf, v.len, dst, dst_len, 1); + if (len < 0) len = -3; // Failed to decode + } + } + return len; } -static bool ff_rename(const char *from, const char *to) { - return f_rename(from, to) == FR_OK; +static bool isx(int c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); } -static bool ff_remove(const char *path) { - return f_unlink(path) == FR_OK; +int mg_url_decode(const char *src, size_t src_len, char *dst, size_t dst_len, + int is_form_url_encoded) { + size_t i, j; + for (i = j = 0; i < src_len && j + 1 < dst_len; i++, j++) { + if (src[i] == '%') { + // Use `i + 2 < src_len`, not `i < src_len - 2`, note small src_len + if (i + 2 < src_len && isx(src[i + 1]) && isx(src[i + 2])) { + mg_str_to_num(mg_str_n(src + i + 1, 2), 16, &dst[j], sizeof(uint8_t)); + i += 2; + } else { + return -1; + } + } else if (is_form_url_encoded && src[i] == '+') { + dst[j] = ' '; + } else { + dst[j] = src[i]; + } + } + if (j < dst_len) dst[j] = '\0'; // Null-terminate the destination + return i >= src_len && j < dst_len ? (int) j : -1; } -static bool ff_mkdir(const char *path) { - return f_mkdir(path) == FR_OK; +static bool isok(uint8_t c) { + return c == '\n' || c == '\r' || c == '\t' || c >= ' '; } -struct mg_fs mg_fs_fat = {ff_stat, ff_list, ff_open, ff_close, ff_read, - ff_write, ff_seek, ff_rename, ff_remove, ff_mkdir}; -#endif - -#ifdef MG_ENABLE_LINES -#line 1 "src/fs_packed.c" -#endif - - - - -struct packed_file { - const char *data; - size_t size; - size_t pos; -}; - -#if MG_ENABLE_PACKED_FS -#else -const char *mg_unpack(const char *path, size_t *size, time_t *mtime) { - *size = 0, *mtime = 0; - (void) path; - return NULL; +int mg_http_get_request_len(const unsigned char *buf, size_t buf_len) { + size_t i; + for (i = 0; i < buf_len; i++) { + if (!isok(buf[i])) return -1; + if ((i > 0 && buf[i] == '\n' && buf[i - 1] == '\n') || + (i > 3 && buf[i] == '\n' && buf[i - 1] == '\r' && buf[i - 2] == '\n')) + return (int) i + 1; + } + return 0; } -const char *mg_unlist(size_t no) { - (void) no; +struct mg_str *mg_http_get_header(struct mg_http_message *h, const char *name) { + size_t i, n = strlen(name), max = sizeof(h->headers) / sizeof(h->headers[0]); + for (i = 0; i < max && h->headers[i].name.len > 0; i++) { + struct mg_str *k = &h->headers[i].name, *v = &h->headers[i].value; + if (n == k->len && mg_ncasecmp(k->buf, name, n) == 0) return v; + } return NULL; } -#endif -struct mg_str mg_unpacked(const char *path) { - size_t len = 0; - const char *buf = mg_unpack(path, &len, NULL); - return mg_str_n(buf, len); +// Is it a valid utf-8 continuation byte +static bool vcb(uint8_t c) { + return (c & 0xc0) == 0x80; } -static int is_dir_prefix(const char *prefix, size_t n, const char *path) { - // MG_INFO(("[%.*s] [%s] %c", (int) n, prefix, path, path[n])); - return n < strlen(path) && strncmp(prefix, path, n) == 0 && - (n == 0 || path[n] == '/' || path[n - 1] == '/'); -} - -static int packed_stat(const char *path, size_t *size, time_t *mtime) { - const char *p; - size_t i, n = strlen(path); - if (mg_unpack(path, size, mtime)) return MG_FS_READ; // Regular file - // Scan all files. If `path` is a dir prefix for any of them, it's a dir - for (i = 0; (p = mg_unlist(i)) != NULL; i++) { - if (is_dir_prefix(path, n, p)) return MG_FS_DIR; - } +// Get character length (valid utf-8). Used to parse method, URI, headers +static size_t clen(const char *s, const char *end) { + const unsigned char *u = (unsigned char *) s, c = *u; + long n = (long) (end - s); + if (c > ' ' && c < '~') return 1; // Usual ascii printed char + if ((c & 0xe0) == 0xc0 && n > 1 && vcb(u[1])) return 2; // 2-byte UTF8 + if ((c & 0xf0) == 0xe0 && n > 2 && vcb(u[1]) && vcb(u[2])) return 3; + if ((c & 0xf8) == 0xf0 && n > 3 && vcb(u[1]) && vcb(u[2]) && vcb(u[3])) + return 4; return 0; } -static void packed_list(const char *dir, void (*fn)(const char *, void *), - void *userdata) { - char buf[MG_PATH_MAX], tmp[sizeof(buf)]; - const char *path, *begin, *end; - size_t i, n = strlen(dir); - tmp[0] = '\0'; // Previously listed entry - for (i = 0; (path = mg_unlist(i)) != NULL; i++) { - if (!is_dir_prefix(dir, n, path)) continue; - begin = &path[n + 1]; - end = strchr(begin, '/'); - if (end == NULL) end = begin + strlen(begin); - mg_snprintf(buf, sizeof(buf), "%.*s", (int) (end - begin), begin); - buf[sizeof(buf) - 1] = '\0'; - // If this entry has been already listed, skip - // NOTE: we're assuming that file list is sorted alphabetically - if (strcmp(buf, tmp) == 0) continue; - fn(buf, userdata); // Not yet listed, call user function - strcpy(tmp, buf); // And save this entry as listed - } +// Skip until the newline. Return advanced `s`, or NULL on error +static const char *skiptorn(const char *s, const char *end, struct mg_str *v) { + v->buf = (char *) s; + while (s < end && s[0] != '\n' && s[0] != '\r') s++, v->len++; // To newline + if (s >= end || (s[0] == '\r' && s[1] != '\n')) return NULL; // Stray \r + if (s < end && s[0] == '\r') s++; // Skip \r + if (s >= end || *s++ != '\n') return NULL; // Skip \n + return s; } -static void *packed_open(const char *path, int flags) { - size_t size = 0; - const char *data = mg_unpack(path, &size, NULL); - struct packed_file *fp = NULL; - if (data == NULL) return NULL; - if (flags & MG_FS_WRITE) return NULL; - if ((fp = (struct packed_file *) calloc(1, sizeof(*fp))) != NULL) { - fp->size = size; - fp->data = data; +static bool mg_http_parse_headers(const char *s, const char *end, + struct mg_http_header *h, size_t max_hdrs) { + size_t i, n; + for (i = 0; i < max_hdrs; i++) { + struct mg_str k = {NULL, 0}, v = {NULL, 0}; + if (s >= end) return false; + if (s[0] == '\n' || (s[0] == '\r' && s[1] == '\n')) break; + k.buf = (char *) s; + while (s < end && s[0] != ':' && (n = clen(s, end)) > 0) s += n, k.len += n; + if (k.len == 0) return false; // Empty name + if (s >= end || clen(s, end) == 0) return false; // Invalid UTF-8 + if (*s++ != ':') return false; // Invalid, not followed by : + // if (clen(s, end) == 0) return false; // Invalid UTF-8 + while (s < end && (s[0] == ' ' || s[0] == '\t')) s++; // Skip spaces + if ((s = skiptorn(s, end, &v)) == NULL) return false; + while (v.len > 0 && (v.buf[v.len - 1] == ' ' || v.buf[v.len - 1] == '\t')) { + v.len--; // Trim spaces + } + // MG_INFO(("--HH [%.*s] [%.*s]", (int) k.len, k.buf, (int) v.len, v.buf)); + h[i].name = k, h[i].value = v; // Success. Assign values } - return (void *) fp; -} - -static void packed_close(void *fp) { - if (fp != NULL) free(fp); -} - -static size_t packed_read(void *fd, void *buf, size_t len) { - struct packed_file *fp = (struct packed_file *) fd; - if (fp->pos + len > fp->size) len = fp->size - fp->pos; - memcpy(buf, &fp->data[fp->pos], len); - fp->pos += len; - return len; -} - -static size_t packed_write(void *fd, const void *buf, size_t len) { - (void) fd, (void) buf, (void) len; - return 0; + return true; } -static size_t packed_seek(void *fd, size_t offset) { - struct packed_file *fp = (struct packed_file *) fd; - fp->pos = offset; - if (fp->pos > fp->size) fp->pos = fp->size; - return fp->pos; -} +int mg_http_parse(const char *s, size_t len, struct mg_http_message *hm) { + int is_response, req_len = mg_http_get_request_len((unsigned char *) s, len); + const char *end = s == NULL ? NULL : s + req_len, *qs; // Cannot add to NULL + const struct mg_str *cl; + size_t n; -static bool packed_rename(const char *from, const char *to) { - (void) from, (void) to; - return false; -} + memset(hm, 0, sizeof(*hm)); + if (req_len <= 0) return req_len; -static bool packed_remove(const char *path) { - (void) path; - return false; -} + hm->message.buf = hm->head.buf = (char *) s; + hm->body.buf = (char *) end; + hm->head.len = (size_t) req_len; + hm->message.len = hm->body.len = (size_t) -1; // Set body length to infinite -static bool packed_mkdir(const char *path) { - (void) path; - return false; -} + // Parse request line + hm->method.buf = (char *) s; + while (s < end && (n = clen(s, end)) > 0) s += n, hm->method.len += n; + while (s < end && s[0] == ' ') s++; // Skip spaces + hm->uri.buf = (char *) s; + while (s < end && (n = clen(s, end)) > 0) s += n, hm->uri.len += n; + while (s < end && s[0] == ' ') s++; // Skip spaces + if ((s = skiptorn(s, end, &hm->proto)) == NULL) return false; -struct mg_fs mg_fs_packed = { - packed_stat, packed_list, packed_open, packed_close, packed_read, - packed_write, packed_seek, packed_rename, packed_remove, packed_mkdir}; + // If URI contains '?' character, setup query string + if ((qs = (const char *) memchr(hm->uri.buf, '?', hm->uri.len)) != NULL) { + hm->query.buf = (char *) qs + 1; + hm->query.len = (size_t) (&hm->uri.buf[hm->uri.len] - (qs + 1)); + hm->uri.len = (size_t) (qs - hm->uri.buf); + } -#ifdef MG_ENABLE_LINES -#line 1 "src/fs_posix.c" -#endif + // Sanity check. Allow protocol/reason to be empty + // Do this check after hm->method.len and hm->uri.len are finalised + if (hm->method.len == 0 || hm->uri.len == 0) return -1; + if (!mg_http_parse_headers(s, end, hm->headers, + sizeof(hm->headers) / sizeof(hm->headers[0]))) + return -1; // error when parsing + if ((cl = mg_http_get_header(hm, "Content-Length")) != NULL) { + if (mg_to_size_t(*cl, &hm->body.len) == false) return -1; + hm->message.len = (size_t) req_len + hm->body.len; + } -#if MG_ENABLE_POSIX_FS + // mg_http_parse() is used to parse both HTTP requests and HTTP + // responses. If HTTP response does not have Content-Length set, then + // body is read until socket is closed, i.e. body.len is infinite (~0). + // + // For HTTP requests though, according to + // http://tools.ietf.org/html/rfc7231#section-8.1.3, + // only POST and PUT methods have defined body semantics. + // Therefore, if Content-Length is not specified and methods are + // not one of PUT or POST, set body length to 0. + // + // So, if it is HTTP request, and Content-Length is not set, + // and method is not (PUT or POST) then reset body length to zero. + is_response = mg_ncasecmp(hm->method.buf, "HTTP/", 5) == 0; + if (hm->body.len == (size_t) ~0 && !is_response && + mg_strcasecmp(hm->method, mg_str("PUT")) != 0 && + mg_strcasecmp(hm->method, mg_str("POST")) != 0) { + hm->body.len = 0; + hm->message.len = (size_t) req_len; + } -#ifndef MG_STAT_STRUCT -#define MG_STAT_STRUCT stat -#endif + // The 204 (No content) responses also have 0 body length + if (hm->body.len == (size_t) ~0 && is_response && + mg_strcasecmp(hm->uri, mg_str("204")) == 0) { + hm->body.len = 0; + hm->message.len = (size_t) req_len; + } + if (hm->message.len < (size_t) req_len) return -1; // Overflow protection -#ifndef MG_STAT_FUNC -#define MG_STAT_FUNC stat -#endif + return req_len; +} -static int p_stat(const char *path, size_t *size, time_t *mtime) { -#if !defined(S_ISDIR) - MG_ERROR(("stat() API is not supported. %p %p %p", path, size, mtime)); - return 0; -#else -#if MG_ARCH == MG_ARCH_WIN32 - struct _stati64 st; - wchar_t tmp[MG_PATH_MAX]; - MultiByteToWideChar(CP_UTF8, 0, path, -1, tmp, sizeof(tmp) / sizeof(tmp[0])); - if (_wstati64(tmp, &st) != 0) return 0; - // If path is a symlink, windows reports 0 in st.st_size. - // Get a real file size by opening it and jumping to the end - if (st.st_size == 0 && (st.st_mode & _S_IFREG)) { - FILE *fp = _wfopen(tmp, L"rb"); - if (fp != NULL) { - fseek(fp, 0, SEEK_END); - if (ftell(fp) > 0) st.st_size = ftell(fp); // Use _ftelli64 on win10+ - fclose(fp); - } +static void mg_http_vprintf_chunk(struct mg_connection *c, const char *fmt, + va_list *ap) { + size_t len = c->send.len; + mg_send(c, " \r\n", 10); + mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, ap); + if (c->send.len >= len + 10) { + mg_snprintf((char *) c->send.buf + len, 9, "%08lx", c->send.len - len - 10); + c->send.buf[len + 8] = '\r'; + if (c->send.len == len + 10) c->is_resp = 0; // Last chunk, reset marker } -#else - struct MG_STAT_STRUCT st; - if (MG_STAT_FUNC(path, &st) != 0) return 0; -#endif - if (size) *size = (size_t) st.st_size; - if (mtime) *mtime = st.st_mtime; - return MG_FS_READ | MG_FS_WRITE | (S_ISDIR(st.st_mode) ? MG_FS_DIR : 0); -#endif + mg_send(c, "\r\n", 2); } -#if MG_ARCH == MG_ARCH_WIN32 -struct dirent { - char d_name[MAX_PATH]; -}; - -typedef struct win32_dir { - HANDLE handle; - WIN32_FIND_DATAW info; - struct dirent result; -} DIR; +void mg_http_printf_chunk(struct mg_connection *c, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + mg_http_vprintf_chunk(c, fmt, &ap); + va_end(ap); +} -#if 0 -int gettimeofday(struct timeval *tv, void *tz) { - FILETIME ft; - unsigned __int64 tmpres = 0; +void mg_http_write_chunk(struct mg_connection *c, const char *buf, size_t len) { + mg_printf(c, "%lx\r\n", (unsigned long) len); + mg_send(c, buf, len); + mg_send(c, "\r\n", 2); + if (len == 0) c->is_resp = 0; +} - if (tv != NULL) { - GetSystemTimeAsFileTime(&ft); - tmpres |= ft.dwHighDateTime; - tmpres <<= 32; - tmpres |= ft.dwLowDateTime; - tmpres /= 10; // convert into microseconds - tmpres -= (int64_t) 11644473600000000; - tv->tv_sec = (long) (tmpres / 1000000UL); - tv->tv_usec = (long) (tmpres % 1000000UL); +// clang-format off +static const char *mg_http_status_code_str(int status_code) { + switch (status_code) { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 208: return "Already Reported"; + case 226: return "IM Used"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Payload Too Large"; + case 414: return "Request-URI Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 418: return "I'm a teapot"; + case 421: return "Misdirected Request"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 426: return "Upgrade Required"; + case 428: return "Precondition Required"; + case 429: return "Too Many Requests"; + case 431: return "Request Header Fields Too Large"; + case 444: return "Connection Closed Without Response"; + case 451: return "Unavailable For Legal Reasons"; + case 499: return "Client Closed Request"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP Version Not Supported"; + case 506: return "Variant Also Negotiates"; + case 507: return "Insufficient Storage"; + case 508: return "Loop Detected"; + case 510: return "Not Extended"; + case 511: return "Network Authentication Required"; + case 599: return "Network Connect Timeout Error"; + default: return ""; } - (void) tz; - return 0; } -#endif +// clang-format on -static int to_wchar(const char *path, wchar_t *wbuf, size_t wbuf_len) { - int ret; - char buf[MAX_PATH * 2], buf2[MAX_PATH * 2], *p; - strncpy(buf, path, sizeof(buf)); - buf[sizeof(buf) - 1] = '\0'; - // Trim trailing slashes. Leave backslash for paths like "X:\" - p = buf + strlen(buf) - 1; - while (p > buf && p[-1] != ':' && (p[0] == '\\' || p[0] == '/')) *p-- = '\0'; - memset(wbuf, 0, wbuf_len * sizeof(wchar_t)); - ret = MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, (int) wbuf_len); - // Convert back to Unicode. If doubly-converted string does not match the - // original, something is fishy, reject. - WideCharToMultiByte(CP_UTF8, 0, wbuf, (int) wbuf_len, buf2, sizeof(buf2), - NULL, NULL); - if (strcmp(buf, buf2) != 0) { - wbuf[0] = L'\0'; - ret = 0; +void mg_http_reply(struct mg_connection *c, int code, const char *headers, + const char *fmt, ...) { + va_list ap; + size_t len; + mg_printf(c, "HTTP/1.1 %d %s\r\n%sContent-Length: \r\n\r\n", code, + mg_http_status_code_str(code), headers == NULL ? "" : headers); + len = c->send.len; + va_start(ap, fmt); + mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, &ap); + va_end(ap); + if (c->send.len > 16) { + size_t n = mg_snprintf((char *) &c->send.buf[len - 15], 11, "%-10lu", + (unsigned long) (c->send.len - len)); + c->send.buf[len - 15 + n] = ' '; // Change ending 0 to space } - return ret; + c->is_resp = 0; } -DIR *opendir(const char *name) { - DIR *d = NULL; - wchar_t wpath[MAX_PATH]; - DWORD attrs; +static void http_cb(struct mg_connection *, int, void *); +static void restore_http_cb(struct mg_connection *c) { + mg_fs_close((struct mg_fd *) c->pfn_data); + c->pfn_data = NULL; + c->pfn = http_cb; + c->is_resp = 0; +} - if (name == NULL) { - SetLastError(ERROR_BAD_ARGUMENTS); - } else if ((d = (DIR *) calloc(1, sizeof(*d))) == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - } else { - to_wchar(name, wpath, sizeof(wpath) / sizeof(wpath[0])); - attrs = GetFileAttributesW(wpath); - if (attrs != 0Xffffffff && (attrs & FILE_ATTRIBUTE_DIRECTORY)) { - (void) wcscat(wpath, L"\\*"); - d->handle = FindFirstFileW(wpath, &d->info); - d->result.d_name[0] = '\0'; - } else { - free(d); - d = NULL; - } +char *mg_http_etag(char *buf, size_t len, size_t size, time_t mtime); +char *mg_http_etag(char *buf, size_t len, size_t size, time_t mtime) { + mg_snprintf(buf, len, "\"%lld.%lld\"", (int64_t) mtime, (int64_t) size); + return buf; +} + +static void static_cb(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_WRITE || ev == MG_EV_POLL) { + struct mg_fd *fd = (struct mg_fd *) c->pfn_data; + // Read to send IO buffer directly, avoid extra on-stack buffer + size_t n, max = MG_IO_SIZE, space; + size_t *cl = (size_t *) &c->data[(sizeof(c->data) - sizeof(size_t)) / + sizeof(size_t) * sizeof(size_t)]; + if (c->send.size < max) mg_iobuf_resize(&c->send, max); + if (c->send.len >= c->send.size) return; // Rate limit + if ((space = c->send.size - c->send.len) > *cl) space = *cl; + n = fd->fs->rd(fd->fd, c->send.buf + c->send.len, space); + c->send.len += n; + *cl -= n; + if (n == 0) restore_http_cb(c); + } else if (ev == MG_EV_CLOSE) { + restore_http_cb(c); } - return d; + (void) ev_data; } -int closedir(DIR *d) { - int result = 0; - if (d != NULL) { - if (d->handle != INVALID_HANDLE_VALUE) - result = FindClose(d->handle) ? 0 : -1; - free(d); - } else { - result = -1; - SetLastError(ERROR_BAD_ARGUMENTS); +// Known mime types. Keep it outside guess_content_type() function, since +// some environments don't like it defined there. +// clang-format off +#define MG_C_STR(a) { (char *) (a), sizeof(a) - 1 } +static struct mg_str s_known_types[] = { + MG_C_STR("html"), MG_C_STR("text/html; charset=utf-8"), + MG_C_STR("htm"), MG_C_STR("text/html; charset=utf-8"), + MG_C_STR("css"), MG_C_STR("text/css; charset=utf-8"), + MG_C_STR("js"), MG_C_STR("text/javascript; charset=utf-8"), + MG_C_STR("gif"), MG_C_STR("image/gif"), + MG_C_STR("png"), MG_C_STR("image/png"), + MG_C_STR("jpg"), MG_C_STR("image/jpeg"), + MG_C_STR("jpeg"), MG_C_STR("image/jpeg"), + MG_C_STR("woff"), MG_C_STR("font/woff"), + MG_C_STR("ttf"), MG_C_STR("font/ttf"), + MG_C_STR("svg"), MG_C_STR("image/svg+xml"), + MG_C_STR("txt"), MG_C_STR("text/plain; charset=utf-8"), + MG_C_STR("avi"), MG_C_STR("video/x-msvideo"), + MG_C_STR("csv"), MG_C_STR("text/csv"), + MG_C_STR("doc"), MG_C_STR("application/msword"), + MG_C_STR("exe"), MG_C_STR("application/octet-stream"), + MG_C_STR("gz"), MG_C_STR("application/gzip"), + MG_C_STR("ico"), MG_C_STR("image/x-icon"), + MG_C_STR("json"), MG_C_STR("application/json"), + MG_C_STR("mov"), MG_C_STR("video/quicktime"), + MG_C_STR("mp3"), MG_C_STR("audio/mpeg"), + MG_C_STR("mp4"), MG_C_STR("video/mp4"), + MG_C_STR("mpeg"), MG_C_STR("video/mpeg"), + MG_C_STR("pdf"), MG_C_STR("application/pdf"), + MG_C_STR("shtml"), MG_C_STR("text/html; charset=utf-8"), + MG_C_STR("tgz"), MG_C_STR("application/tar-gz"), + MG_C_STR("wav"), MG_C_STR("audio/wav"), + MG_C_STR("webp"), MG_C_STR("image/webp"), + MG_C_STR("zip"), MG_C_STR("application/zip"), + MG_C_STR("3gp"), MG_C_STR("video/3gpp"), + {0, 0}, +}; +// clang-format on + +static struct mg_str guess_content_type(struct mg_str path, const char *extra) { + struct mg_str entry, k, v, s = mg_str(extra), asterisk = mg_str_n("*", 1); + size_t i = 0; + + // Shrink path to its extension only + while (i < path.len && path.buf[path.len - i - 1] != '.') i++; + path.buf += path.len - i; + path.len = i; + + // Process user-provided mime type overrides, if any + while (mg_span(s, &entry, &s, ',')) { + if (mg_span(entry, &k, &v, '=') && + (mg_strcmp(asterisk, k) == 0 || mg_strcmp(path, k) == 0)) + return v; } - return result; + + // Process built-in mime types + for (i = 0; s_known_types[i].buf != NULL; i += 2) { + if (mg_strcmp(path, s_known_types[i]) == 0) return s_known_types[i + 1]; + } + + return mg_str("text/plain; charset=utf-8"); } -struct dirent *readdir(DIR *d) { - struct dirent *result = NULL; - if (d != NULL) { - memset(&d->result, 0, sizeof(d->result)); - if (d->handle != INVALID_HANDLE_VALUE) { - result = &d->result; - WideCharToMultiByte(CP_UTF8, 0, d->info.cFileName, -1, result->d_name, - sizeof(result->d_name), NULL, NULL); - if (!FindNextFileW(d->handle, &d->info)) { - FindClose(d->handle); - d->handle = INVALID_HANDLE_VALUE; - } +static int getrange(struct mg_str *s, size_t *a, size_t *b) { + size_t i, numparsed = 0; + for (i = 0; i + 6 < s->len; i++) { + struct mg_str k, v = mg_str_n(s->buf + i + 6, s->len - i - 6); + if (memcmp(&s->buf[i], "bytes=", 6) != 0) continue; + if (mg_span(v, &k, &v, '-')) { + if (mg_to_size_t(k, a)) numparsed++; + if (v.len > 0 && mg_to_size_t(v, b)) numparsed++; } else { - SetLastError(ERROR_FILE_NOT_FOUND); + if (mg_to_size_t(v, a)) numparsed++; } - } else { - SetLastError(ERROR_BAD_ARGUMENTS); + break; } - return result; + return (int) numparsed; } -#endif -static void p_list(const char *dir, void (*fn)(const char *, void *), - void *userdata) { -#if MG_ENABLE_DIRLIST - struct dirent *dp; - DIR *dirp; - if ((dirp = (opendir(dir))) == NULL) return; - while ((dp = readdir(dirp)) != NULL) { - if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) continue; - fn(dp->d_name, userdata); +void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, + const char *path, + const struct mg_http_serve_opts *opts) { + char etag[64], tmp[MG_PATH_MAX]; + struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; + struct mg_fd *fd = NULL; + size_t size = 0; + time_t mtime = 0; + struct mg_str *inm = NULL; + struct mg_str mime = guess_content_type(mg_str(path), opts->mime_types); + bool gzip = false; + + if (path != NULL) { + // If a browser sends us "Accept-Encoding: gzip", try to open .gz first + struct mg_str *ae = mg_http_get_header(hm, "Accept-Encoding"); + if (ae != NULL) { + char *ae_ = mg_mprintf("%.*s", ae->len, ae->buf); + if (ae_ != NULL && strstr(ae_, "gzip") != NULL) { + mg_snprintf(tmp, sizeof(tmp), "%s.gz", path); + fd = mg_fs_open(fs, tmp, MG_FS_READ); + if (fd != NULL) gzip = true, path = tmp; + } + free(ae_); + } + // No luck opening .gz? Open what we've told to open + if (fd == NULL) fd = mg_fs_open(fs, path, MG_FS_READ); } - closedir(dirp); -#else - (void) dir, (void) fn, (void) userdata; -#endif -} -static void *p_open(const char *path, int flags) { -#if MG_ARCH == MG_ARCH_WIN32 - const char *mode = flags == MG_FS_READ ? "rb" : "a+b"; - wchar_t b1[MG_PATH_MAX], b2[10]; - MultiByteToWideChar(CP_UTF8, 0, path, -1, b1, sizeof(b1) / sizeof(b1[0])); - MultiByteToWideChar(CP_UTF8, 0, mode, -1, b2, sizeof(b2) / sizeof(b2[0])); - return (void *) _wfopen(b1, b2); -#else - const char *mode = flags == MG_FS_READ ? "rbe" : "a+be"; // e for CLOEXEC - return (void *) fopen(path, mode); -#endif -} + // Failed to open, and page404 is configured? Open it, then + if (fd == NULL && opts->page404 != NULL) { + fd = mg_fs_open(fs, opts->page404, MG_FS_READ); + path = opts->page404; + mime = guess_content_type(mg_str(path), opts->mime_types); + } -static void p_close(void *fp) { - fclose((FILE *) fp); -} + if (fd == NULL || fs->st(path, &size, &mtime) == 0) { + mg_http_reply(c, 404, opts->extra_headers, "Not found\n"); + mg_fs_close(fd); + // NOTE: mg_http_etag() call should go first! + } else if (mg_http_etag(etag, sizeof(etag), size, mtime) != NULL && + (inm = mg_http_get_header(hm, "If-None-Match")) != NULL && + mg_strcasecmp(*inm, mg_str(etag)) == 0) { + mg_fs_close(fd); + mg_http_reply(c, 304, opts->extra_headers, ""); + } else { + int n, status = 200; + char range[100]; + size_t r1 = 0, r2 = 0, cl = size; -static size_t p_read(void *fp, void *buf, size_t len) { - return fread(buf, 1, len, (FILE *) fp); + // Handle Range header + struct mg_str *rh = mg_http_get_header(hm, "Range"); + range[0] = '\0'; + if (rh != NULL && (n = getrange(rh, &r1, &r2)) > 0) { + // If range is specified like "400-", set second limit to content len + if (n == 1) r2 = cl - 1; + if (r1 > r2 || r2 >= cl) { + status = 416; + cl = 0; + mg_snprintf(range, sizeof(range), "Content-Range: bytes */%lld\r\n", + (int64_t) size); + } else { + status = 206; + cl = r2 - r1 + 1; + mg_snprintf(range, sizeof(range), + "Content-Range: bytes %llu-%llu/%llu\r\n", (uint64_t) r1, + (uint64_t) (r1 + cl - 1), (uint64_t) size); + fs->sk(fd->fd, r1); + } + } + mg_printf(c, + "HTTP/1.1 %d %s\r\n" + "Content-Type: %.*s\r\n" + "Etag: %s\r\n" + "Content-Length: %llu\r\n" + "%s%s%s\r\n", + status, mg_http_status_code_str(status), (int) mime.len, mime.buf, + etag, (uint64_t) cl, gzip ? "Content-Encoding: gzip\r\n" : "", + range, opts->extra_headers ? opts->extra_headers : ""); + if (mg_strcasecmp(hm->method, mg_str("HEAD")) == 0) { + c->is_resp = 0; + mg_fs_close(fd); + } else { + // Track to-be-sent content length at the end of c->data, aligned + size_t *clp = (size_t *) &c->data[(sizeof(c->data) - sizeof(size_t)) / + sizeof(size_t) * sizeof(size_t)]; + c->pfn = static_cb; + c->pfn_data = fd; + *clp = cl; + } + } } -static size_t p_write(void *fp, const void *buf, size_t len) { - return fwrite(buf, 1, len, (FILE *) fp); -} +struct printdirentrydata { + struct mg_connection *c; + struct mg_http_message *hm; + const struct mg_http_serve_opts *opts; + const char *dir; +}; -static size_t p_seek(void *fp, size_t offset) { -#if (defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64) || \ - (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 200112L) || \ - (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 600) - if (fseeko((FILE *) fp, (off_t) offset, SEEK_SET) != 0) (void) 0; +#if MG_ENABLE_DIRLIST +static void printdirentry(const char *name, void *userdata) { + struct printdirentrydata *d = (struct printdirentrydata *) userdata; + struct mg_fs *fs = d->opts->fs == NULL ? &mg_fs_posix : d->opts->fs; + size_t size = 0; + time_t t = 0; + char path[MG_PATH_MAX], sz[40], mod[40]; + int flags, n = 0; + + // MG_DEBUG(("[%s] [%s]", d->dir, name)); + if (mg_snprintf(path, sizeof(path), "%s%c%s", d->dir, '/', name) > + sizeof(path)) { + MG_ERROR(("%s truncated", name)); + } else if ((flags = fs->st(path, &size, &t)) == 0) { + MG_ERROR(("%lu stat(%s): %d", d->c->id, path, errno)); + } else { + const char *slash = flags & MG_FS_DIR ? "/" : ""; + if (flags & MG_FS_DIR) { + mg_snprintf(sz, sizeof(sz), "%s", "[DIR]"); + } else { + mg_snprintf(sz, sizeof(sz), "%lld", (uint64_t) size); + } +#if defined(MG_HTTP_DIRLIST_TIME_FMT) + { + char time_str[40]; + struct tm *time_info = localtime(&t); + strftime(time_str, sizeof time_str, "%Y/%m/%d %H:%M:%S", time_info); + mg_snprintf(mod, sizeof(mod), "%s", time_str); + } #else - if (fseek((FILE *) fp, (long) offset, SEEK_SET) != 0) (void) 0; + mg_snprintf(mod, sizeof(mod), "%lu", (unsigned long) t); #endif - return (size_t) ftell((FILE *) fp); + n = (int) mg_url_encode(name, strlen(name), path, sizeof(path)); + mg_printf(d->c, + " %s%s" + "%s%s\n", + n, path, slash, name, slash, (unsigned long) t, mod, + flags & MG_FS_DIR ? (int64_t) -1 : (int64_t) size, sz); + } } -static bool p_rename(const char *from, const char *to) { - return rename(from, to) == 0; -} +static void listdir(struct mg_connection *c, struct mg_http_message *hm, + const struct mg_http_serve_opts *opts, char *dir) { + const char *sort_js_code = + ""; + struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; + struct printdirentrydata d = {c, hm, opts, dir}; + char tmp[10], buf[MG_PATH_MAX]; + size_t off, n; + int len = mg_url_decode(hm->uri.buf, hm->uri.len, buf, sizeof(buf), 0); + struct mg_str uri = len > 0 ? mg_str_n(buf, (size_t) len) : hm->uri; -static bool p_remove(const char *path) { - return remove(path) == 0; -} + mg_printf(c, + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "%s" + "Content-Length: \r\n\r\n", + opts->extra_headers == NULL ? "" : opts->extra_headers); + off = c->send.len; // Start of body + mg_printf(c, + "Index of %.*s%s%s" + "" + "

Index of %.*s

" + "" + "" + "" + "\n", + (int) uri.len, uri.buf, sort_js_code, sort_js_code2, + c->mgr->directory_listing_css, (int) uri.len, uri.buf); + mg_printf(c, "%s", + " " + "\n"); -static bool p_mkdir(const char *path) { - return mkdir(path, 0775) == 0; + fs->ls(dir, printdirentry, &d); + mg_printf(c, + "" + "
Name" + "ModifiedSize
..[DIR]
%s
\n", + c->mgr->product_name); + n = mg_snprintf(tmp, sizeof(tmp), "%lu", (unsigned long) (c->send.len - off)); + if (n > sizeof(tmp)) n = 0; + memcpy(c->send.buf + off - 12, tmp, n); // Set content length + c->is_resp = 0; // Mark response end } +#endif -#else - -static int p_stat(const char *path, size_t *size, time_t *mtime) { - (void) path, (void) size, (void) mtime; - return 0; -} -static void p_list(const char *path, void (*fn)(const char *, void *), - void *userdata) { - (void) path, (void) fn, (void) userdata; -} -static void *p_open(const char *path, int flags) { - (void) path, (void) flags; - return NULL; -} -static void p_close(void *fp) { - (void) fp; -} -static size_t p_read(void *fd, void *buf, size_t len) { - (void) fd, (void) buf, (void) len; - return 0; -} -static size_t p_write(void *fd, const void *buf, size_t len) { - (void) fd, (void) buf, (void) len; - return 0; -} -static size_t p_seek(void *fd, size_t offset) { - (void) fd, (void) offset; - return (size_t) ~0; -} -static bool p_rename(const char *from, const char *to) { - (void) from, (void) to; - return false; -} -static bool p_remove(const char *path) { - (void) path; - return false; -} -static bool p_mkdir(const char *path) { - (void) path; - return false; +// Resolve requested file into `path` and return its fs->st() result +static int uri_to_path2(struct mg_connection *c, struct mg_http_message *hm, + struct mg_fs *fs, struct mg_str url, struct mg_str dir, + char *path, size_t path_size) { + int flags, tmp; + // Append URI to the root_dir, and sanitize it + size_t n = mg_snprintf(path, path_size, "%.*s", (int) dir.len, dir.buf); + if (n + 2 >= path_size) { + mg_http_reply(c, 400, "", "Exceeded path size"); + return -1; + } + path[path_size - 1] = '\0'; + // Terminate root dir with slash + if (n > 0 && path[n - 1] != '/') path[n++] = '/', path[n] = '\0'; + if (url.len < hm->uri.len) { + mg_url_decode(hm->uri.buf + url.len, hm->uri.len - url.len, path + n, + path_size - n, 0); + } + path[path_size - 1] = '\0'; // Double-check + if (!mg_path_is_sane(mg_str_n(path, path_size))) { + mg_http_reply(c, 400, "", "Invalid path"); + return -1; + } + n = strlen(path); + while (n > 1 && path[n - 1] == '/') path[--n] = 0; // Trim trailing slashes + flags = mg_strcmp(hm->uri, mg_str("/")) == 0 ? MG_FS_DIR + : fs->st(path, NULL, NULL); + MG_VERBOSE(("%lu %.*s -> %s %d", c->id, (int) hm->uri.len, hm->uri.buf, path, + flags)); + if (flags == 0) { + // Do nothing - let's caller decide + } else if ((flags & MG_FS_DIR) && hm->uri.len > 0 && + hm->uri.buf[hm->uri.len - 1] != '/') { + mg_printf(c, + "HTTP/1.1 301 Moved\r\n" + "Location: %.*s/\r\n" + "Content-Length: 0\r\n" + "\r\n", + (int) hm->uri.len, hm->uri.buf); + c->is_resp = 0; + flags = -1; + } else if (flags & MG_FS_DIR) { + if (((mg_snprintf(path + n, path_size - n, "/" MG_HTTP_INDEX) > 0 && + (tmp = fs->st(path, NULL, NULL)) != 0) || + (mg_snprintf(path + n, path_size - n, "/index.shtml") > 0 && + (tmp = fs->st(path, NULL, NULL)) != 0))) { + flags = tmp; + } else if ((mg_snprintf(path + n, path_size - n, "/" MG_HTTP_INDEX ".gz") > + 0 && + (tmp = fs->st(path, NULL, NULL)) != + 0)) { // check for gzipped index + flags = tmp; + path[n + 1 + strlen(MG_HTTP_INDEX)] = + '\0'; // Remove appended .gz in index file name + } else { + path[n] = '\0'; // Remove appended index file name + } + } + return flags; } -#endif -struct mg_fs mg_fs_posix = {p_stat, p_list, p_open, p_close, p_read, - p_write, p_seek, p_rename, p_remove, p_mkdir}; +static int uri_to_path(struct mg_connection *c, struct mg_http_message *hm, + const struct mg_http_serve_opts *opts, char *path, + size_t path_size) { + struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; + struct mg_str k, v, part, s = mg_str(opts->root_dir), u = {NULL, 0}, p = u; + while (mg_span(s, &part, &s, ',')) { + if (!mg_span(part, &k, &v, '=')) k = part, v = mg_str_n(NULL, 0); + if (v.len == 0) v = k, k = mg_str("/"), u = k, p = v; + if (hm->uri.len < k.len) continue; + if (mg_strcmp(k, mg_str_n(hm->uri.buf, k.len)) != 0) continue; + u = k, p = v; + } + return uri_to_path2(c, hm, fs, u, p, path, path_size); +} -#ifdef MG_ENABLE_LINES -#line 1 "src/http.c" +void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm, + const struct mg_http_serve_opts *opts) { + char path[MG_PATH_MAX]; + const char *sp = opts->ssi_pattern; + int flags = uri_to_path(c, hm, opts, path, sizeof(path)); + if (flags < 0) { + // Do nothing: the response has already been sent by uri_to_path() + } else if (flags & MG_FS_DIR) { +#if MG_ENABLE_DIRLIST + listdir(c, hm, opts, path); +#else + mg_http_reply(c, 403, "", "Forbidden\n"); #endif + } else if (flags && sp != NULL && mg_match(mg_str(path), mg_str(sp), NULL)) { + mg_http_serve_ssi(c, opts->root_dir, path); + } else { + mg_http_serve_file(c, hm, path, opts); + } +} - - - - - - - - - - - - -static int mg_ncasecmp(const char *s1, const char *s2, size_t len) { - int diff = 0; - if (len > 0) do { - int c = *s1++, d = *s2++; - if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; - if (d >= 'A' && d <= 'Z') d += 'a' - 'A'; - diff = c - d; - } while (diff == 0 && s1[-1] != '\0' && --len > 0); - return diff; +static bool mg_is_url_safe(int c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || c == '.' || c == '_' || c == '-' || c == '~'; } -bool mg_to_size_t(struct mg_str str, size_t *val); -bool mg_to_size_t(struct mg_str str, size_t *val) { - size_t i = 0, max = (size_t) -1, max2 = max / 10, result = 0, ndigits = 0; - while (i < str.len && (str.buf[i] == ' ' || str.buf[i] == '\t')) i++; - if (i < str.len && str.buf[i] == '-') return false; - while (i < str.len && str.buf[i] >= '0' && str.buf[i] <= '9') { - size_t digit = (size_t) (str.buf[i] - '0'); - if (result > max2) return false; // Overflow - result *= 10; - if (result > max - digit) return false; // Overflow - result += digit; - i++, ndigits++; +size_t mg_url_encode(const char *s, size_t sl, char *buf, size_t len) { + size_t i, n = 0; + for (i = 0; i < sl; i++) { + int c = *(unsigned char *) &s[i]; + if (n + 4 >= len) return 0; + if (mg_is_url_safe(c)) { + buf[n++] = s[i]; + } else { + mg_snprintf(&buf[n], 4, "%%%M", mg_print_hex, 1, &s[i]); + n += 3; + } } - while (i < str.len && (str.buf[i] == ' ' || str.buf[i] == '\t')) i++; - if (ndigits == 0) return false; // #2322: Content-Length = 1 * DIGIT - if (i != str.len) return false; // Ditto - *val = (size_t) result; - return true; + if (len > 0 && n < len - 1) buf[n] = '\0'; // Null-terminate the destination + if (len > 0) buf[len - 1] = '\0'; // Always. + return n; } -// Chunk deletion marker is the MSB in the "processed" counter -#define MG_DMARK ((size_t) 1 << (sizeof(size_t) * 8 - 1)) - -// Multipart POST example: -// --xyz -// Content-Disposition: form-data; name="val" -// -// abcdef -// --xyz -// Content-Disposition: form-data; name="foo"; filename="a.txt" -// Content-Type: text/plain -// -// hello world -// -// --xyz-- -size_t mg_http_next_multipart(struct mg_str body, size_t ofs, - struct mg_http_part *part) { - struct mg_str cd = mg_str_n("Content-Disposition", 19); - const char *s = body.buf; - size_t b = ofs, h1, h2, b1, b2, max = body.len; - - // Init part params - if (part != NULL) part->name = part->filename = part->body = mg_str_n(0, 0); - - // Skip boundary - while (b + 2 < max && s[b] != '\r' && s[b + 1] != '\n') b++; - if (b <= ofs || b + 2 >= max) return 0; - // MG_INFO(("B: %zu %zu [%.*s]", ofs, b - ofs, (int) (b - ofs), s)); - - // Skip headers - h1 = h2 = b + 2; - for (;;) { - while (h2 + 2 < max && s[h2] != '\r' && s[h2 + 1] != '\n') h2++; - if (h2 == h1) break; - if (h2 + 2 >= max) return 0; - // MG_INFO(("Header: [%.*s]", (int) (h2 - h1), &s[h1])); - if (part != NULL && h1 + cd.len + 2 < h2 && s[h1 + cd.len] == ':' && - mg_ncasecmp(&s[h1], cd.buf, cd.len) == 0) { - struct mg_str v = mg_str_n(&s[h1 + cd.len + 2], h2 - (h1 + cd.len + 2)); - part->name = mg_http_get_header_var(v, mg_str_n("name", 4)); - part->filename = mg_http_get_header_var(v, mg_str_n("filename", 8)); +void mg_http_creds(struct mg_http_message *hm, char *user, size_t userlen, + char *pass, size_t passlen) { + struct mg_str *v = mg_http_get_header(hm, "Authorization"); + user[0] = pass[0] = '\0'; + if (v != NULL && v->len > 6 && memcmp(v->buf, "Basic ", 6) == 0) { + char buf[256]; + size_t n = mg_base64_decode(v->buf + 6, v->len - 6, buf, sizeof(buf)); + const char *p = (const char *) memchr(buf, ':', n > 0 ? n : 0); + if (p != NULL) { + mg_snprintf(user, userlen, "%.*s", p - buf, buf); + mg_snprintf(pass, passlen, "%.*s", n - (size_t) (p - buf) - 1, p + 1); } - h1 = h2 = h2 + 2; + } else if (v != NULL && v->len > 7 && memcmp(v->buf, "Bearer ", 7) == 0) { + mg_snprintf(pass, passlen, "%.*s", (int) v->len - 7, v->buf + 7); + } else if ((v = mg_http_get_header(hm, "Cookie")) != NULL) { + struct mg_str t = mg_http_get_header_var(*v, mg_str_n("access_token", 12)); + if (t.len > 0) mg_snprintf(pass, passlen, "%.*s", (int) t.len, t.buf); + } else { + mg_http_get_var(&hm->query, "access_token", pass, passlen); } - b1 = b2 = h2 + 2; - while (b2 + 2 + (b - ofs) + 2 < max && !(s[b2] == '\r' && s[b2 + 1] == '\n' && - memcmp(&s[b2 + 2], s, b - ofs) == 0)) - b2++; +} - if (b2 + 2 >= max) return 0; - if (part != NULL) part->body = mg_str_n(&s[b1], b2 - b1); - // MG_INFO(("Body: [%.*s]", (int) (b2 - b1), &s[b1])); - return b2 + 2; +static struct mg_str stripquotes(struct mg_str s) { + return s.len > 1 && s.buf[0] == '"' && s.buf[s.len - 1] == '"' + ? mg_str_n(s.buf + 1, s.len - 2) + : s; } -void mg_http_bauth(struct mg_connection *c, const char *user, - const char *pass) { - struct mg_str u = mg_str(user), p = mg_str(pass); - size_t need = c->send.len + 36 + (u.len + p.len) * 2; - if (c->send.size < need) mg_iobuf_resize(&c->send, need); - if (c->send.size >= need) { - size_t i, n = 0; - char *buf = (char *) &c->send.buf[c->send.len]; - memcpy(buf, "Authorization: Basic ", 21); // DON'T use mg_send! - for (i = 0; i < u.len; i++) { - n = mg_base64_update(((unsigned char *) u.buf)[i], buf + 21, n); - } - if (p.len > 0) { - n = mg_base64_update(':', buf + 21, n); - for (i = 0; i < p.len; i++) { - n = mg_base64_update(((unsigned char *) p.buf)[i], buf + 21, n); - } - } - n = mg_base64_final(buf + 21, n); - c->send.len += 21 + (size_t) n + 2; - memcpy(&c->send.buf[c->send.len - 2], "\r\n", 2); - } else { - MG_ERROR(("%lu oom %d->%d ", c->id, (int) c->send.size, (int) need)); - } -} - -struct mg_str mg_http_var(struct mg_str buf, struct mg_str name) { - struct mg_str entry, k, v, result = mg_str_n(NULL, 0); - while (mg_span(buf, &entry, &buf, '&')) { - if (mg_span(entry, &k, &v, '=') && name.len == k.len && - mg_ncasecmp(name.buf, k.buf, k.len) == 0) { - result = v; - break; +struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v) { + size_t i; + for (i = 0; v.len > 0 && i + v.len + 2 < s.len; i++) { + if (s.buf[i + v.len] == '=' && memcmp(&s.buf[i], v.buf, v.len) == 0) { + const char *p = &s.buf[i + v.len + 1], *b = p, *x = &s.buf[s.len]; + int q = p < x && *p == '"' ? 1 : 0; + while (p < x && + (q ? p == b || *p != '"' : *p != ';' && *p != ' ' && *p != ',')) + p++; + // MG_INFO(("[%.*s] [%.*s] [%.*s]", (int) s.len, s.buf, (int) v.len, + // v.buf, (int) (p - b), b)); + return stripquotes(mg_str_n(b, (size_t) (p - b + q))); } } - return result; + return mg_str_n(NULL, 0); } -int mg_http_get_var(const struct mg_str *buf, const char *name, char *dst, - size_t dst_len) { - int len; - if (dst != NULL && dst_len > 0) { - dst[0] = '\0'; // If destination buffer is valid, always nul-terminate it - } - if (dst == NULL || dst_len == 0) { - len = -2; // Bad destination - } else if (buf->buf == NULL || name == NULL || buf->len == 0) { - len = -1; // Bad source +long mg_http_upload(struct mg_connection *c, struct mg_http_message *hm, + struct mg_fs *fs, const char *dir, size_t max_size) { + char buf[20] = "0", file[MG_PATH_MAX], path[MG_PATH_MAX]; + long res = 0, offset; + mg_http_get_var(&hm->query, "offset", buf, sizeof(buf)); + mg_http_get_var(&hm->query, "file", file, sizeof(file)); + offset = strtol(buf, NULL, 0); + mg_snprintf(path, sizeof(path), "%s%c%s", dir, MG_DIRSEP, file); + if (hm->body.len == 0) { + mg_http_reply(c, 200, "", "%ld", res); // Nothing to write + } else if (file[0] == '\0') { + mg_http_reply(c, 400, "", "file required"); + res = -1; + } else if (mg_path_is_sane(mg_str(file)) == false) { + mg_http_reply(c, 400, "", "%s: invalid file", file); + res = -2; + } else if (offset < 0) { + mg_http_reply(c, 400, "", "offset required"); + res = -3; + } else if ((size_t) offset + hm->body.len > max_size) { + mg_http_reply(c, 400, "", "%s: over max size of %lu", path, + (unsigned long) max_size); + res = -4; } else { - struct mg_str v = mg_http_var(*buf, mg_str(name)); - if (v.buf == NULL) { - len = -4; // Name does not exist + struct mg_fd *fd; + size_t current_size = 0; + MG_DEBUG(("%s -> %lu bytes @ %ld", path, hm->body.len, offset)); + if (offset == 0) fs->rm(path); // If offset if 0, truncate file + fs->st(path, ¤t_size, NULL); + if (offset > 0 && current_size != (size_t) offset) { + mg_http_reply(c, 400, "", "%s: offset mismatch", path); + res = -5; + } else if ((fd = mg_fs_open(fs, path, MG_FS_WRITE)) == NULL) { + mg_http_reply(c, 400, "", "open(%s): %d", path, errno); + res = -6; } else { - len = mg_url_decode(v.buf, v.len, dst, dst_len, 1); - if (len < 0) len = -3; // Failed to decode + res = offset + (long) fs->wr(fd->fd, hm->body.buf, hm->body.len); + mg_fs_close(fd); + mg_http_reply(c, 200, "", "%ld", res); } } - return len; + return res; } -static bool isx(int c) { +int mg_http_status(const struct mg_http_message *hm) { + return atoi(hm->uri.buf); +} + +static bool is_hex_digit(int c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } -int mg_url_decode(const char *src, size_t src_len, char *dst, size_t dst_len, - int is_form_url_encoded) { - size_t i, j; - for (i = j = 0; i < src_len && j + 1 < dst_len; i++, j++) { - if (src[i] == '%') { - // Use `i + 2 < src_len`, not `i < src_len - 2`, note small src_len - if (i + 2 < src_len && isx(src[i + 1]) && isx(src[i + 2])) { - mg_str_to_num(mg_str_n(src + i + 1, 2), 16, &dst[j], sizeof(uint8_t)); - i += 2; - } else { - return -1; +static int skip_chunk(const char *buf, int len, int *pl, int *dl) { + int i = 0, n = 0; + if (len < 3) return 0; + while (i < len && is_hex_digit(buf[i])) i++; + if (i == 0) return -1; // Error, no length specified + if (i > (int) sizeof(int) * 2) return -1; // Chunk length is too big + if (len < i + 1 || buf[i] != '\r' || buf[i + 1] != '\n') return -1; // Error + if (mg_str_to_num(mg_str_n(buf, (size_t) i), 16, &n, sizeof(int)) == false) + return -1; // Decode chunk length, overflow + if (n < 0) return -1; // Error. TODO(): some checks now redundant + if (n > len - i - 4) return 0; // Chunk not yet fully buffered + if (buf[i + n + 2] != '\r' || buf[i + n + 3] != '\n') return -1; // Error + *pl = i + 2, *dl = n; + return i + 2 + n + 2; +} + +static void http_cb(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_READ || ev == MG_EV_CLOSE || + (ev == MG_EV_POLL && c->is_accepted && !c->is_draining && + c->recv.len > 0)) { // see #2796 + struct mg_http_message hm; + size_t ofs = 0; // Parsing offset + while (c->is_resp == 0 && ofs < c->recv.len) { + const char *buf = (char *) c->recv.buf + ofs; + int n = mg_http_parse(buf, c->recv.len - ofs, &hm); + struct mg_str *te; // Transfer - encoding header + bool is_chunked = false; + size_t old_len = c->recv.len; + if (n < 0) { + // We don't use mg_error() here, to avoid closing pipelined requests + // prematurely, see #2592 + MG_ERROR(("HTTP parse, %lu bytes", c->recv.len)); + c->is_draining = 1; + mg_hexdump(buf, c->recv.len - ofs > 16 ? 16 : c->recv.len - ofs); + c->recv.len = 0; + return; + } + if (n == 0) break; // Request is not buffered yet + mg_call(c, MG_EV_HTTP_HDRS, &hm); // Got all HTTP headers + if (c->recv.len != old_len) { + // User manipulated received data. Wash our hands + MG_DEBUG(("%lu detaching HTTP handler", c->id)); + c->pfn = NULL; + return; + } + if (ev == MG_EV_CLOSE) { // If client did not set Content-Length + hm.message.len = c->recv.len - ofs; // and closes now, deliver MSG + hm.body.len = hm.message.len - (size_t) (hm.body.buf - hm.message.buf); + } + if ((te = mg_http_get_header(&hm, "Transfer-Encoding")) != NULL) { + if (mg_strcasecmp(*te, mg_str("chunked")) == 0) { + is_chunked = true; + } else { + mg_error(c, "Invalid Transfer-Encoding"); // See #2460 + return; + } + } else if (mg_http_get_header(&hm, "Content-length") == NULL) { + // #2593: HTTP packets must contain either Transfer-Encoding or + // Content-length + bool is_response = mg_ncasecmp(hm.method.buf, "HTTP/", 5) == 0; + bool require_content_len = false; + if (!is_response && (mg_strcasecmp(hm.method, mg_str("POST")) == 0 || + mg_strcasecmp(hm.method, mg_str("PUT")) == 0)) { + // POST and PUT should include an entity body. Therefore, they should + // contain a Content-length header. Other requests can also contain a + // body, but their content has no defined semantics (RFC 7231) + require_content_len = true; + ofs += (size_t) n; // this request has been processed + } else if (is_response) { + // HTTP spec 7.2 Entity body: All other responses must include a body + // or Content-Length header field defined with a value of 0. + int status = mg_http_status(&hm); + require_content_len = status >= 200 && status != 204 && status != 304; + } + if (require_content_len) { + if (!c->is_client) mg_http_reply(c, 411, "", ""); + MG_ERROR(("Content length missing from %s", is_response ? "response" : "request")); + } + } + + if (is_chunked) { + // For chunked data, strip off prefixes and suffixes from chunks + // and relocate them right after the headers, then report a message + char *s = (char *) c->recv.buf + ofs + n; + int o = 0, pl, dl, cl, len = (int) (c->recv.len - ofs - (size_t) n); + + // Find zero-length chunk (the end of the body) + while ((cl = skip_chunk(s + o, len - o, &pl, &dl)) > 0 && dl) o += cl; + if (cl == 0) break; // No zero-len chunk, buffer more data + if (cl < 0) { + mg_error(c, "Invalid chunk"); + break; + } + + // Zero chunk found. Second pass: strip + relocate + o = 0, hm.body.len = 0, hm.message.len = (size_t) n; + while ((cl = skip_chunk(s + o, len - o, &pl, &dl)) > 0) { + memmove(s + hm.body.len, s + o + pl, (size_t) dl); + o += cl, hm.body.len += (size_t) dl, hm.message.len += (size_t) dl; + if (dl == 0) break; + } + ofs += (size_t) (n + o); + } else { // Normal, non-chunked data + size_t len = c->recv.len - ofs - (size_t) n; + if (hm.body.len > len) break; // Buffer more data + ofs += (size_t) n + hm.body.len; + } + + if (c->is_accepted) c->is_resp = 1; // Start generating response + mg_call(c, MG_EV_HTTP_MSG, &hm); // User handler can clear is_resp + if (c->is_accepted && !c->is_resp) { + struct mg_str *cc = mg_http_get_header(&hm, "Connection"); + if (cc != NULL && mg_strcasecmp(*cc, mg_str("close")) == 0) { + c->is_draining = 1; // honor "Connection: close" + break; + } } - } else if (is_form_url_encoded && src[i] == '+') { - dst[j] = ' '; - } else { - dst[j] = src[i]; } + if (ofs > 0) mg_iobuf_del(&c->recv, 0, ofs); // Delete processed data } - if (j < dst_len) dst[j] = '\0'; // Null-terminate the destination - return i >= src_len && j < dst_len ? (int) j : -1; + (void) ev_data; } -static bool isok(uint8_t c) { - return c == '\n' || c == '\r' || c == '\t' || c >= ' '; +static void mg_hfn(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_HTTP_MSG) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + if (mg_match(hm->uri, mg_str("/quit"), NULL)) { + mg_http_reply(c, 200, "", "ok\n"); + c->is_draining = 1; + c->data[0] = 'X'; + } else if (mg_match(hm->uri, mg_str("/debug"), NULL)) { + int level = (int) mg_json_get_long(hm->body, "$.level", MG_LL_DEBUG); + mg_log_set(level); + mg_http_reply(c, 200, "", "Debug level set to %d\n", level); + } else { + mg_http_reply(c, 200, "", "hi\n"); + } + } else if (ev == MG_EV_CLOSE) { + if (c->data[0] == 'X') *(bool *) c->fn_data = true; + } } -int mg_http_get_request_len(const unsigned char *buf, size_t buf_len) { - size_t i; - for (i = 0; i < buf_len; i++) { - if (!isok(buf[i])) return -1; - if ((i > 0 && buf[i] == '\n' && buf[i - 1] == '\n') || - (i > 3 && buf[i] == '\n' && buf[i - 1] == '\r' && buf[i - 2] == '\n')) - return (int) i + 1; - } - return 0; -} -struct mg_str *mg_http_get_header(struct mg_http_message *h, const char *name) { - size_t i, n = strlen(name), max = sizeof(h->headers) / sizeof(h->headers[0]); - for (i = 0; i < max && h->headers[i].name.len > 0; i++) { - struct mg_str *k = &h->headers[i].name, *v = &h->headers[i].value; - if (n == k->len && mg_ncasecmp(k->buf, name, n) == 0) return v; - } - return NULL; +void mg_hello(const char *url) { + struct mg_mgr mgr; + bool done = false; + mg_mgr_init(&mgr); + if (mg_http_listen(&mgr, url, mg_hfn, &done) == NULL) done = true; + while (done == false) mg_mgr_poll(&mgr, 100); + mg_mgr_free(&mgr); } -// Is it a valid utf-8 continuation byte -static bool vcb(uint8_t c) { - return (c & 0xc0) == 0x80; +struct mg_connection *mg_http_connect(struct mg_mgr *mgr, const char *url, + mg_event_handler_t fn, void *fn_data) { + struct mg_connection *c = mg_connect(mgr, url, fn, fn_data); + if (c != NULL) c->pfn = http_cb; + return c; } -// Get character length (valid utf-8). Used to parse method, URI, headers -static size_t clen(const char *s, const char *end) { - const unsigned char *u = (unsigned char *) s, c = *u; - long n = (long) (end - s); - if (c > ' ' && c < '~') return 1; // Usual ascii printed char - if ((c & 0xe0) == 0xc0 && n > 1 && vcb(u[1])) return 2; // 2-byte UTF8 - if ((c & 0xf0) == 0xe0 && n > 2 && vcb(u[1]) && vcb(u[2])) return 3; - if ((c & 0xf8) == 0xf0 && n > 3 && vcb(u[1]) && vcb(u[2]) && vcb(u[3])) - return 4; - return 0; +struct mg_connection *mg_http_listen(struct mg_mgr *mgr, const char *url, + mg_event_handler_t fn, void *fn_data) { + struct mg_connection *c = mg_listen(mgr, url, fn, fn_data); + if (c != NULL) c->pfn = http_cb; + return c; } -// Skip until the newline. Return advanced `s`, or NULL on error -static const char *skiptorn(const char *s, const char *end, struct mg_str *v) { - v->buf = (char *) s; - while (s < end && s[0] != '\n' && s[0] != '\r') s++, v->len++; // To newline - if (s >= end || (s[0] == '\r' && s[1] != '\n')) return NULL; // Stray \r - if (s < end && s[0] == '\r') s++; // Skip \r - if (s >= end || *s++ != '\n') return NULL; // Skip \n - return s; +#ifdef MG_ENABLE_LINES +#line 1 "src/iobuf.c" +#endif + + + + + +static size_t roundup(size_t size, size_t align) { + return align == 0 ? size : (size + align - 1) / align * align; } -static bool mg_http_parse_headers(const char *s, const char *end, - struct mg_http_header *h, size_t max_hdrs) { - size_t i, n; - for (i = 0; i < max_hdrs; i++) { - struct mg_str k = {NULL, 0}, v = {NULL, 0}; - if (s >= end) return false; - if (s[0] == '\n' || (s[0] == '\r' && s[1] == '\n')) break; - k.buf = (char *) s; - while (s < end && s[0] != ':' && (n = clen(s, end)) > 0) s += n, k.len += n; - if (k.len == 0) return false; // Empty name - if (s >= end || clen(s, end) == 0) return false; // Invalid UTF-8 - if (*s++ != ':') return false; // Invalid, not followed by : - // if (clen(s, end) == 0) return false; // Invalid UTF-8 - while (s < end && (s[0] == ' ' || s[0] == '\t')) s++; // Skip spaces - if ((s = skiptorn(s, end, &v)) == NULL) return false; - while (v.len > 0 && (v.buf[v.len - 1] == ' ' || v.buf[v.len - 1] == '\t')) { - v.len--; // Trim spaces +int mg_iobuf_resize(struct mg_iobuf *io, size_t new_size) { + int ok = 1; + new_size = roundup(new_size, io->align); + if (new_size == 0) { + mg_bzero(io->buf, io->size); + free(io->buf); + io->buf = NULL; + io->len = io->size = 0; + } else if (new_size != io->size) { + // NOTE(lsm): do not use realloc here. Use calloc/free only, to ease the + // porting to some obscure platforms like FreeRTOS + void *p = calloc(1, new_size); + if (p != NULL) { + size_t len = new_size < io->len ? new_size : io->len; + if (len > 0 && io->buf != NULL) memmove(p, io->buf, len); + mg_bzero(io->buf, io->size); + free(io->buf); + io->buf = (unsigned char *) p; + io->size = new_size; + } else { + ok = 0; + MG_ERROR(("%lld->%lld", (uint64_t) io->size, (uint64_t) new_size)); } - // MG_INFO(("--HH [%.*s] [%.*s]", (int) k.len, k.buf, (int) v.len, v.buf)); - h[i].name = k, h[i].value = v; // Success. Assign values } - return true; + return ok; } -int mg_http_parse(const char *s, size_t len, struct mg_http_message *hm) { - int is_response, req_len = mg_http_get_request_len((unsigned char *) s, len); - const char *end = s == NULL ? NULL : s + req_len, *qs; // Cannot add to NULL - const struct mg_str *cl; - size_t n; - - memset(hm, 0, sizeof(*hm)); - if (req_len <= 0) return req_len; +int mg_iobuf_init(struct mg_iobuf *io, size_t size, size_t align) { + io->buf = NULL; + io->align = align; + io->size = io->len = 0; + return mg_iobuf_resize(io, size); +} - hm->message.buf = hm->head.buf = (char *) s; - hm->body.buf = (char *) end; - hm->head.len = (size_t) req_len; - hm->message.len = hm->body.len = (size_t) -1; // Set body length to infinite +size_t mg_iobuf_add(struct mg_iobuf *io, size_t ofs, const void *buf, + size_t len) { + size_t new_size = roundup(io->len + len, io->align); + mg_iobuf_resize(io, new_size); // Attempt to resize + if (new_size != io->size) len = 0; // Resize failure, append nothing + if (ofs < io->len) memmove(io->buf + ofs + len, io->buf + ofs, io->len - ofs); + if (buf != NULL) memmove(io->buf + ofs, buf, len); + if (ofs > io->len) io->len += ofs - io->len; + io->len += len; + return len; +} - // Parse request line - hm->method.buf = (char *) s; - while (s < end && (n = clen(s, end)) > 0) s += n, hm->method.len += n; - while (s < end && s[0] == ' ') s++; // Skip spaces - hm->uri.buf = (char *) s; - while (s < end && (n = clen(s, end)) > 0) s += n, hm->uri.len += n; - while (s < end && s[0] == ' ') s++; // Skip spaces - if ((s = skiptorn(s, end, &hm->proto)) == NULL) return false; +size_t mg_iobuf_del(struct mg_iobuf *io, size_t ofs, size_t len) { + if (ofs > io->len) ofs = io->len; + if (ofs + len > io->len) len = io->len - ofs; + if (io->buf) memmove(io->buf + ofs, io->buf + ofs + len, io->len - ofs - len); + if (io->buf) mg_bzero(io->buf + io->len - len, len); + io->len -= len; + return len; +} - // If URI contains '?' character, setup query string - if ((qs = (const char *) memchr(hm->uri.buf, '?', hm->uri.len)) != NULL) { - hm->query.buf = (char *) qs + 1; - hm->query.len = (size_t) (&hm->uri.buf[hm->uri.len] - (qs + 1)); - hm->uri.len = (size_t) (qs - hm->uri.buf); - } +void mg_iobuf_free(struct mg_iobuf *io) { + mg_iobuf_resize(io, 0); +} - // Sanity check. Allow protocol/reason to be empty - // Do this check after hm->method.len and hm->uri.len are finalised - if (hm->method.len == 0 || hm->uri.len == 0) return -1; +#ifdef MG_ENABLE_LINES +#line 1 "src/json.c" +#endif - if (!mg_http_parse_headers(s, end, hm->headers, - sizeof(hm->headers) / sizeof(hm->headers[0]))) - return -1; // error when parsing - if ((cl = mg_http_get_header(hm, "Content-Length")) != NULL) { - if (mg_to_size_t(*cl, &hm->body.len) == false) return -1; - hm->message.len = (size_t) req_len + hm->body.len; - } - // mg_http_parse() is used to parse both HTTP requests and HTTP - // responses. If HTTP response does not have Content-Length set, then - // body is read until socket is closed, i.e. body.len is infinite (~0). - // - // For HTTP requests though, according to - // http://tools.ietf.org/html/rfc7231#section-8.1.3, - // only POST and PUT methods have defined body semantics. - // Therefore, if Content-Length is not specified and methods are - // not one of PUT or POST, set body length to 0. - // - // So, if it is HTTP request, and Content-Length is not set, - // and method is not (PUT or POST) then reset body length to zero. - is_response = mg_ncasecmp(hm->method.buf, "HTTP/", 5) == 0; - if (hm->body.len == (size_t) ~0 && !is_response && - mg_strcasecmp(hm->method, mg_str("PUT")) != 0 && - mg_strcasecmp(hm->method, mg_str("POST")) != 0) { - hm->body.len = 0; - hm->message.len = (size_t) req_len; - } - // The 204 (No content) responses also have 0 body length - if (hm->body.len == (size_t) ~0 && is_response && - mg_strcasecmp(hm->uri, mg_str("204")) == 0) { - hm->body.len = 0; - hm->message.len = (size_t) req_len; - } - if (hm->message.len < (size_t) req_len) return -1; // Overflow protection - return req_len; +static const char *escapeseq(int esc) { + return esc ? "\b\f\n\r\t\\\"" : "bfnrt\\\""; } -static void mg_http_vprintf_chunk(struct mg_connection *c, const char *fmt, - va_list *ap) { - size_t len = c->send.len; - mg_send(c, " \r\n", 10); - mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, ap); - if (c->send.len >= len + 10) { - mg_snprintf((char *) c->send.buf + len, 9, "%08lx", c->send.len - len - 10); - c->send.buf[len + 8] = '\r'; - if (c->send.len == len + 10) c->is_resp = 0; // Last chunk, reset marker +static char json_esc(int c, int esc) { + const char *p, *esc1 = escapeseq(esc), *esc2 = escapeseq(!esc); + for (p = esc1; *p != '\0'; p++) { + if (*p == c) return esc2[p - esc1]; } - mg_send(c, "\r\n", 2); + return 0; } -void mg_http_printf_chunk(struct mg_connection *c, const char *fmt, ...) { - va_list ap; - va_start(ap, fmt); - mg_http_vprintf_chunk(c, fmt, &ap); - va_end(ap); +static int mg_pass_string(const char *s, int len) { + int i; + for (i = 0; i < len; i++) { + if (s[i] == '\\' && i + 1 < len && json_esc(s[i + 1], 1)) { + i++; + } else if (s[i] == '\0') { + return MG_JSON_INVALID; + } else if (s[i] == '"') { + return i; + } + } + return MG_JSON_INVALID; } -void mg_http_write_chunk(struct mg_connection *c, const char *buf, size_t len) { - mg_printf(c, "%lx\r\n", (unsigned long) len); - mg_send(c, buf, len); - mg_send(c, "\r\n", 2); - if (len == 0) c->is_resp = 0; -} +static double mg_atod(const char *p, int len, int *numlen) { + double d = 0.0; + int i = 0, sign = 1; -// clang-format off -static const char *mg_http_status_code_str(int status_code) { - switch (status_code) { - case 100: return "Continue"; - case 101: return "Switching Protocols"; - case 102: return "Processing"; - case 200: return "OK"; - case 201: return "Created"; - case 202: return "Accepted"; - case 203: return "Non-authoritative Information"; - case 204: return "No Content"; - case 205: return "Reset Content"; - case 206: return "Partial Content"; - case 207: return "Multi-Status"; - case 208: return "Already Reported"; - case 226: return "IM Used"; - case 300: return "Multiple Choices"; - case 301: return "Moved Permanently"; - case 302: return "Found"; - case 303: return "See Other"; - case 304: return "Not Modified"; - case 305: return "Use Proxy"; - case 307: return "Temporary Redirect"; - case 308: return "Permanent Redirect"; - case 400: return "Bad Request"; - case 401: return "Unauthorized"; - case 402: return "Payment Required"; - case 403: return "Forbidden"; - case 404: return "Not Found"; - case 405: return "Method Not Allowed"; - case 406: return "Not Acceptable"; - case 407: return "Proxy Authentication Required"; - case 408: return "Request Timeout"; - case 409: return "Conflict"; - case 410: return "Gone"; - case 411: return "Length Required"; - case 412: return "Precondition Failed"; - case 413: return "Payload Too Large"; - case 414: return "Request-URI Too Long"; - case 415: return "Unsupported Media Type"; - case 416: return "Requested Range Not Satisfiable"; - case 417: return "Expectation Failed"; - case 418: return "I'm a teapot"; - case 421: return "Misdirected Request"; - case 422: return "Unprocessable Entity"; - case 423: return "Locked"; - case 424: return "Failed Dependency"; - case 426: return "Upgrade Required"; - case 428: return "Precondition Required"; - case 429: return "Too Many Requests"; - case 431: return "Request Header Fields Too Large"; - case 444: return "Connection Closed Without Response"; - case 451: return "Unavailable For Legal Reasons"; - case 499: return "Client Closed Request"; - case 500: return "Internal Server Error"; - case 501: return "Not Implemented"; - case 502: return "Bad Gateway"; - case 503: return "Service Unavailable"; - case 504: return "Gateway Timeout"; - case 505: return "HTTP Version Not Supported"; - case 506: return "Variant Also Negotiates"; - case 507: return "Insufficient Storage"; - case 508: return "Loop Detected"; - case 510: return "Not Extended"; - case 511: return "Network Authentication Required"; - case 599: return "Network Connect Timeout Error"; - default: return ""; - } -} -// clang-format on - -void mg_http_reply(struct mg_connection *c, int code, const char *headers, - const char *fmt, ...) { - va_list ap; - size_t len; - mg_printf(c, "HTTP/1.1 %d %s\r\n%sContent-Length: \r\n\r\n", code, - mg_http_status_code_str(code), headers == NULL ? "" : headers); - len = c->send.len; - va_start(ap, fmt); - mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, &ap); - va_end(ap); - if (c->send.len > 16) { - size_t n = mg_snprintf((char *) &c->send.buf[len - 15], 11, "%-10lu", - (unsigned long) (c->send.len - len)); - c->send.buf[len - 15 + n] = ' '; // Change ending 0 to space + // Sign + if (i < len && *p == '-') { + sign = -1, i++; + } else if (i < len && *p == '+') { + i++; } - c->is_resp = 0; -} - -static void http_cb(struct mg_connection *, int, void *); -static void restore_http_cb(struct mg_connection *c) { - mg_fs_close((struct mg_fd *) c->pfn_data); - c->pfn_data = NULL; - c->pfn = http_cb; - c->is_resp = 0; -} - -char *mg_http_etag(char *buf, size_t len, size_t size, time_t mtime); -char *mg_http_etag(char *buf, size_t len, size_t size, time_t mtime) { - mg_snprintf(buf, len, "\"%lld.%lld\"", (int64_t) mtime, (int64_t) size); - return buf; -} -static void static_cb(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_WRITE || ev == MG_EV_POLL) { - struct mg_fd *fd = (struct mg_fd *) c->pfn_data; - // Read to send IO buffer directly, avoid extra on-stack buffer - size_t n, max = MG_IO_SIZE, space; - size_t *cl = (size_t *) &c->data[(sizeof(c->data) - sizeof(size_t)) / - sizeof(size_t) * sizeof(size_t)]; - if (c->send.size < max) mg_iobuf_resize(&c->send, max); - if (c->send.len >= c->send.size) return; // Rate limit - if ((space = c->send.size - c->send.len) > *cl) space = *cl; - n = fd->fs->rd(fd->fd, c->send.buf + c->send.len, space); - c->send.len += n; - *cl -= n; - if (n == 0) restore_http_cb(c); - } else if (ev == MG_EV_CLOSE) { - restore_http_cb(c); + // Decimal + for (; i < len && p[i] >= '0' && p[i] <= '9'; i++) { + d *= 10.0; + d += p[i] - '0'; } - (void) ev_data; -} - -// Known mime types. Keep it outside guess_content_type() function, since -// some environments don't like it defined there. -// clang-format off -#define MG_C_STR(a) { (char *) (a), sizeof(a) - 1 } -static struct mg_str s_known_types[] = { - MG_C_STR("html"), MG_C_STR("text/html; charset=utf-8"), - MG_C_STR("htm"), MG_C_STR("text/html; charset=utf-8"), - MG_C_STR("css"), MG_C_STR("text/css; charset=utf-8"), - MG_C_STR("js"), MG_C_STR("text/javascript; charset=utf-8"), - MG_C_STR("gif"), MG_C_STR("image/gif"), - MG_C_STR("png"), MG_C_STR("image/png"), - MG_C_STR("jpg"), MG_C_STR("image/jpeg"), - MG_C_STR("jpeg"), MG_C_STR("image/jpeg"), - MG_C_STR("woff"), MG_C_STR("font/woff"), - MG_C_STR("ttf"), MG_C_STR("font/ttf"), - MG_C_STR("svg"), MG_C_STR("image/svg+xml"), - MG_C_STR("txt"), MG_C_STR("text/plain; charset=utf-8"), - MG_C_STR("avi"), MG_C_STR("video/x-msvideo"), - MG_C_STR("csv"), MG_C_STR("text/csv"), - MG_C_STR("doc"), MG_C_STR("application/msword"), - MG_C_STR("exe"), MG_C_STR("application/octet-stream"), - MG_C_STR("gz"), MG_C_STR("application/gzip"), - MG_C_STR("ico"), MG_C_STR("image/x-icon"), - MG_C_STR("json"), MG_C_STR("application/json"), - MG_C_STR("mov"), MG_C_STR("video/quicktime"), - MG_C_STR("mp3"), MG_C_STR("audio/mpeg"), - MG_C_STR("mp4"), MG_C_STR("video/mp4"), - MG_C_STR("mpeg"), MG_C_STR("video/mpeg"), - MG_C_STR("pdf"), MG_C_STR("application/pdf"), - MG_C_STR("shtml"), MG_C_STR("text/html; charset=utf-8"), - MG_C_STR("tgz"), MG_C_STR("application/tar-gz"), - MG_C_STR("wav"), MG_C_STR("audio/wav"), - MG_C_STR("webp"), MG_C_STR("image/webp"), - MG_C_STR("zip"), MG_C_STR("application/zip"), - MG_C_STR("3gp"), MG_C_STR("video/3gpp"), - {0, 0}, -}; -// clang-format on - -static struct mg_str guess_content_type(struct mg_str path, const char *extra) { - struct mg_str entry, k, v, s = mg_str(extra), asterisk = mg_str_n("*", 1); - size_t i = 0; - - // Shrink path to its extension only - while (i < path.len && path.buf[path.len - i - 1] != '.') i++; - path.buf += path.len - i; - path.len = i; + d *= sign; - // Process user-provided mime type overrides, if any - while (mg_span(s, &entry, &s, ',')) { - if (mg_span(entry, &k, &v, '=') && - (mg_strcmp(asterisk, k) == 0 || mg_strcmp(path, k) == 0)) - return v; + // Fractional + if (i < len && p[i] == '.') { + double frac = 0.0, base = 0.1; + i++; + for (; i < len && p[i] >= '0' && p[i] <= '9'; i++) { + frac += base * (p[i] - '0'); + base /= 10.0; + } + d += frac * sign; } - // Process built-in mime types - for (i = 0; s_known_types[i].buf != NULL; i += 2) { - if (mg_strcmp(path, s_known_types[i]) == 0) return s_known_types[i + 1]; + // Exponential + if (i < len && (p[i] == 'e' || p[i] == 'E')) { + int j, exp = 0, minus = 0; + i++; + if (i < len && p[i] == '-') minus = 1, i++; + if (i < len && p[i] == '+') i++; + while (i < len && p[i] >= '0' && p[i] <= '9' && exp < 308) + exp = exp * 10 + (p[i++] - '0'); + if (minus) exp = -exp; + for (j = 0; j < exp; j++) d *= 10.0; + for (j = 0; j < -exp; j++) d /= 10.0; } - return mg_str("text/plain; charset=utf-8"); + if (numlen != NULL) *numlen = i; + return d; } -static int getrange(struct mg_str *s, size_t *a, size_t *b) { - size_t i, numparsed = 0; - for (i = 0; i + 6 < s->len; i++) { - struct mg_str k, v = mg_str_n(s->buf + i + 6, s->len - i - 6); - if (memcmp(&s->buf[i], "bytes=", 6) != 0) continue; - if (mg_span(v, &k, &v, '-')) { - if (mg_to_size_t(k, a)) numparsed++; - if (v.len > 0 && mg_to_size_t(v, b)) numparsed++; - } else { - if (mg_to_size_t(v, a)) numparsed++; +// Iterate over object or array elements +size_t mg_json_next(struct mg_str obj, size_t ofs, struct mg_str *key, + struct mg_str *val) { + if (ofs >= obj.len) { + ofs = 0; // Out of boundaries, stop scanning + } else if (obj.len < 2 || (*obj.buf != '{' && *obj.buf != '[')) { + ofs = 0; // Not an array or object, stop + } else { + struct mg_str sub = mg_str_n(obj.buf + ofs, obj.len - ofs); + if (ofs == 0) ofs++, sub.buf++, sub.len--; + if (*obj.buf == '[') { // Iterate over an array + int n = 0, o = mg_json_get(sub, "$", &n); + if (n < 0 || o < 0 || (size_t) (o + n) > sub.len) { + ofs = 0; // Error parsing key, stop scanning + } else { + if (key) *key = mg_str_n(NULL, 0); + if (val) *val = mg_str_n(sub.buf + o, (size_t) n); + ofs = (size_t) (&sub.buf[o + n] - obj.buf); + } + } else { // Iterate over an object + int n = 0, o = mg_json_get(sub, "$", &n); + if (n < 0 || o < 0 || (size_t) (o + n) > sub.len) { + ofs = 0; // Error parsing key, stop scanning + } else { + if (key) *key = mg_str_n(sub.buf + o, (size_t) n); + sub.buf += o + n, sub.len -= (size_t) (o + n); + while (sub.len > 0 && *sub.buf != ':') sub.len--, sub.buf++; + if (sub.len > 0 && *sub.buf == ':') sub.len--, sub.buf++; + n = 0, o = mg_json_get(sub, "$", &n); + if (n < 0 || o < 0 || (size_t) (o + n) > sub.len) { + ofs = 0; // Error parsing value, stop scanning + } else { + if (val) *val = mg_str_n(sub.buf + o, (size_t) n); + ofs = (size_t) (&sub.buf[o + n] - obj.buf); + } + } } - break; + // MG_INFO(("SUB ofs %u %.*s", ofs, sub.len, sub.buf)); + while (ofs && ofs < obj.len && + (obj.buf[ofs] == ' ' || obj.buf[ofs] == '\t' || + obj.buf[ofs] == '\n' || obj.buf[ofs] == '\r')) { + ofs++; + } + if (ofs && ofs < obj.len && obj.buf[ofs] == ',') ofs++; + if (ofs > obj.len) ofs = 0; } - return (int) numparsed; + return ofs; } -void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, - const char *path, - const struct mg_http_serve_opts *opts) { - char etag[64], tmp[MG_PATH_MAX]; - struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; - struct mg_fd *fd = NULL; - size_t size = 0; - time_t mtime = 0; - struct mg_str *inm = NULL; - struct mg_str mime = guess_content_type(mg_str(path), opts->mime_types); - bool gzip = false; +int mg_json_get(struct mg_str json, const char *path, int *toklen) { + const char *s = json.buf; + int len = (int) json.len; + enum { S_VALUE, S_KEY, S_COLON, S_COMMA_OR_EOO } expecting = S_VALUE; + unsigned char nesting[MG_JSON_MAX_DEPTH]; + int i = 0; // Current offset in `s` + int j = 0; // Offset in `s` we're looking for (return value) + int depth = 0; // Current depth (nesting level) + int ed = 0; // Expected depth + int pos = 1; // Current position in `path` + int ci = -1, ei = -1; // Current and expected index in array - if (path != NULL) { - // If a browser sends us "Accept-Encoding: gzip", try to open .gz first - struct mg_str *ae = mg_http_get_header(hm, "Accept-Encoding"); - if (ae != NULL) { - char *ae_ = mg_mprintf("%.*s", ae->len, ae->buf); - if (ae_ != NULL && strstr(ae_, "gzip") != NULL) { - mg_snprintf(tmp, sizeof(tmp), "%s.gz", path); - fd = mg_fs_open(fs, tmp, MG_FS_READ); - if (fd != NULL) gzip = true, path = tmp; - } - free(ae_); + if (toklen) *toklen = 0; + if (path[0] != '$') return MG_JSON_INVALID; + +#define MG_CHECKRET(x) \ + do { \ + if (depth == ed && path[pos] == '\0' && ci == ei) { \ + if (toklen) *toklen = i - j + 1; \ + return j; \ + } \ + } while (0) + +// In the ascii table, the distance between `[` and `]` is 2. +// Ditto for `{` and `}`. Hence +2 in the code below. +#define MG_EOO(x) \ + do { \ + if (depth == ed && ci != ei) return MG_JSON_NOT_FOUND; \ + if (c != nesting[depth - 1] + 2) return MG_JSON_INVALID; \ + depth--; \ + MG_CHECKRET(x); \ + } while (0) + + for (i = 0; i < len; i++) { + unsigned char c = ((unsigned char *) s)[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue; + switch (expecting) { + case S_VALUE: + // p("V %s [%.*s] %d %d %d %d\n", path, pos, path, depth, ed, ci, ei); + if (depth == ed) j = i; + if (c == '{') { + if (depth >= (int) sizeof(nesting)) return MG_JSON_TOO_DEEP; + if (depth == ed && path[pos] == '.' && ci == ei) { + // If we start the object, reset array indices + ed++, pos++, ci = ei = -1; + } + nesting[depth++] = c; + expecting = S_KEY; + break; + } else if (c == '[') { + if (depth >= (int) sizeof(nesting)) return MG_JSON_TOO_DEEP; + if (depth == ed && path[pos] == '[' && ei == ci) { + ed++, pos++, ci = 0; + for (ei = 0; path[pos] != ']' && path[pos] != '\0'; pos++) { + ei *= 10; + ei += path[pos] - '0'; + } + if (path[pos] != 0) pos++; + } + nesting[depth++] = c; + break; + } else if (c == ']' && depth > 0) { // Empty array + MG_EOO(']'); + } else if (c == 't' && i + 3 < len && memcmp(&s[i], "true", 4) == 0) { + i += 3; + } else if (c == 'n' && i + 3 < len && memcmp(&s[i], "null", 4) == 0) { + i += 3; + } else if (c == 'f' && i + 4 < len && memcmp(&s[i], "false", 5) == 0) { + i += 4; + } else if (c == '-' || ((c >= '0' && c <= '9'))) { + int numlen = 0; + mg_atod(&s[i], len - i, &numlen); + i += numlen - 1; + } else if (c == '"') { + int n = mg_pass_string(&s[i + 1], len - i - 1); + if (n < 0) return n; + i += n + 1; + } else { + return MG_JSON_INVALID; + } + MG_CHECKRET('V'); + if (depth == ed && ei >= 0) ci++; + expecting = S_COMMA_OR_EOO; + break; + + case S_KEY: + if (c == '"') { + int n = mg_pass_string(&s[i + 1], len - i - 1); + if (n < 0) return n; + if (i + 1 + n >= len) return MG_JSON_NOT_FOUND; + if (depth < ed) return MG_JSON_NOT_FOUND; + if (depth == ed && path[pos - 1] != '.') return MG_JSON_NOT_FOUND; + // printf("K %s [%.*s] [%.*s] %d %d %d %d %d\n", path, pos, path, n, + // &s[i + 1], n, depth, ed, ci, ei); + // NOTE(cpq): in the check sequence below is important. + // strncmp() must go first: it fails fast if the remaining length + // of the path is smaller than `n`. + if (depth == ed && path[pos - 1] == '.' && + strncmp(&s[i + 1], &path[pos], (size_t) n) == 0 && + (path[pos + n] == '\0' || path[pos + n] == '.' || + path[pos + n] == '[')) { + pos += n; + } + i += n + 1; + expecting = S_COLON; + } else if (c == '}') { // Empty object + MG_EOO('}'); + expecting = S_COMMA_OR_EOO; + if (depth == ed && ei >= 0) ci++; + } else { + return MG_JSON_INVALID; + } + break; + + case S_COLON: + if (c == ':') { + expecting = S_VALUE; + } else { + return MG_JSON_INVALID; + } + break; + + case S_COMMA_OR_EOO: + if (depth <= 0) { + return MG_JSON_INVALID; + } else if (c == ',') { + expecting = (nesting[depth - 1] == '{') ? S_KEY : S_VALUE; + } else if (c == ']' || c == '}') { + if (depth == ed && c == '}' && path[pos - 1] == '.') + return MG_JSON_NOT_FOUND; + if (depth == ed && c == ']' && path[pos - 1] == ',') + return MG_JSON_NOT_FOUND; + MG_EOO('O'); + if (depth == ed && ei >= 0) ci++; + } else { + return MG_JSON_INVALID; + } + break; } - // No luck opening .gz? Open what we've told to open - if (fd == NULL) fd = mg_fs_open(fs, path, MG_FS_READ); } + return MG_JSON_NOT_FOUND; +} - // Failed to open, and page404 is configured? Open it, then - if (fd == NULL && opts->page404 != NULL) { - fd = mg_fs_open(fs, opts->page404, MG_FS_READ); - path = opts->page404; - mime = guess_content_type(mg_str(path), opts->mime_types); +struct mg_str mg_json_get_tok(struct mg_str json, const char *path) { + int len = 0, ofs = mg_json_get(json, path, &len); + return mg_str_n(ofs < 0 ? NULL : json.buf + ofs, + (size_t) (len < 0 ? 0 : len)); +} + +bool mg_json_get_num(struct mg_str json, const char *path, double *v) { + int n, toklen, found = 0; + if ((n = mg_json_get(json, path, &toklen)) >= 0 && + (json.buf[n] == '-' || (json.buf[n] >= '0' && json.buf[n] <= '9'))) { + if (v != NULL) *v = mg_atod(json.buf + n, toklen, NULL); + found = 1; } + return found; +} - if (fd == NULL || fs->st(path, &size, &mtime) == 0) { - mg_http_reply(c, 404, opts->extra_headers, "Not found\n"); - mg_fs_close(fd); - // NOTE: mg_http_etag() call should go first! - } else if (mg_http_etag(etag, sizeof(etag), size, mtime) != NULL && - (inm = mg_http_get_header(hm, "If-None-Match")) != NULL && - mg_strcasecmp(*inm, mg_str(etag)) == 0) { - mg_fs_close(fd); - mg_http_reply(c, 304, opts->extra_headers, ""); - } else { - int n, status = 200; - char range[100]; - size_t r1 = 0, r2 = 0, cl = size; +bool mg_json_get_bool(struct mg_str json, const char *path, bool *v) { + int found = 0, off = mg_json_get(json, path, NULL); + if (off >= 0 && (json.buf[off] == 't' || json.buf[off] == 'f')) { + if (v != NULL) *v = json.buf[off] == 't'; + found = 1; + } + return found; +} - // Handle Range header - struct mg_str *rh = mg_http_get_header(hm, "Range"); - range[0] = '\0'; - if (rh != NULL && (n = getrange(rh, &r1, &r2)) > 0) { - // If range is specified like "400-", set second limit to content len - if (n == 1) r2 = cl - 1; - if (r1 > r2 || r2 >= cl) { - status = 416; - cl = 0; - mg_snprintf(range, sizeof(range), "Content-Range: bytes */%lld\r\n", - (int64_t) size); - } else { - status = 206; - cl = r2 - r1 + 1; - mg_snprintf(range, sizeof(range), - "Content-Range: bytes %llu-%llu/%llu\r\n", (uint64_t) r1, - (uint64_t) (r1 + cl - 1), (uint64_t) size); - fs->sk(fd->fd, r1); - } - } - mg_printf(c, - "HTTP/1.1 %d %s\r\n" - "Content-Type: %.*s\r\n" - "Etag: %s\r\n" - "Content-Length: %llu\r\n" - "%s%s%s\r\n", - status, mg_http_status_code_str(status), (int) mime.len, mime.buf, - etag, (uint64_t) cl, gzip ? "Content-Encoding: gzip\r\n" : "", - range, opts->extra_headers ? opts->extra_headers : ""); - if (mg_strcasecmp(hm->method, mg_str("HEAD")) == 0) { - c->is_resp = 0; - mg_fs_close(fd); +bool mg_json_unescape(struct mg_str s, char *to, size_t n) { + size_t i, j; + for (i = 0, j = 0; i < s.len && j < n; i++, j++) { + if (s.buf[i] == '\\' && i + 5 < s.len && s.buf[i + 1] == 'u') { + // \uXXXX escape. We process simple one-byte chars \u00xx within ASCII + // range. More complex chars would require dragging in a UTF8 library, + // which is too much for us + if (mg_str_to_num(mg_str_n(s.buf + i + 2, 4), 16, &to[j], + sizeof(uint8_t)) == false) + return false; + i += 5; + } else if (s.buf[i] == '\\' && i + 1 < s.len) { + char c = json_esc(s.buf[i + 1], 0); + if (c == 0) return false; + to[j] = c; + i++; } else { - // Track to-be-sent content length at the end of c->data, aligned - size_t *clp = (size_t *) &c->data[(sizeof(c->data) - sizeof(size_t)) / - sizeof(size_t) * sizeof(size_t)]; - c->pfn = static_cb; - c->pfn_data = fd; - *clp = cl; + to[j] = s.buf[i]; } } + if (j >= n) return false; + if (n > 0) to[j] = '\0'; + return true; } -struct printdirentrydata { - struct mg_connection *c; - struct mg_http_message *hm; - const struct mg_http_serve_opts *opts; - const char *dir; -}; - -#if MG_ENABLE_DIRLIST -static void printdirentry(const char *name, void *userdata) { - struct printdirentrydata *d = (struct printdirentrydata *) userdata; - struct mg_fs *fs = d->opts->fs == NULL ? &mg_fs_posix : d->opts->fs; - size_t size = 0; - time_t t = 0; - char path[MG_PATH_MAX], sz[40], mod[40]; - int flags, n = 0; - - // MG_DEBUG(("[%s] [%s]", d->dir, name)); - if (mg_snprintf(path, sizeof(path), "%s%c%s", d->dir, '/', name) > - sizeof(path)) { - MG_ERROR(("%s truncated", name)); - } else if ((flags = fs->st(path, &size, &t)) == 0) { - MG_ERROR(("%lu stat(%s): %d", d->c->id, path, errno)); - } else { - const char *slash = flags & MG_FS_DIR ? "/" : ""; - if (flags & MG_FS_DIR) { - mg_snprintf(sz, sizeof(sz), "%s", "[DIR]"); - } else { - mg_snprintf(sz, sizeof(sz), "%lld", (uint64_t) size); - } -#if defined(MG_HTTP_DIRLIST_TIME_FMT) - { - char time_str[40]; - struct tm *time_info = localtime(&t); - strftime(time_str, sizeof time_str, "%Y/%m/%d %H:%M:%S", time_info); - mg_snprintf(mod, sizeof(mod), "%s", time_str); +char *mg_json_get_str(struct mg_str json, const char *path) { + char *result = NULL; + int len = 0, off = mg_json_get(json, path, &len); + if (off >= 0 && len > 1 && json.buf[off] == '"') { + if ((result = (char *) calloc(1, (size_t) len)) != NULL && + !mg_json_unescape(mg_str_n(json.buf + off + 1, (size_t) (len - 2)), + result, (size_t) len)) { + free(result); + result = NULL; } -#else - mg_snprintf(mod, sizeof(mod), "%lu", (unsigned long) t); -#endif - n = (int) mg_url_encode(name, strlen(name), path, sizeof(path)); - mg_printf(d->c, - " %s%s" - "%s%s\n", - n, path, slash, name, slash, (unsigned long) t, mod, - flags & MG_FS_DIR ? (int64_t) -1 : (int64_t) size, sz); } + return result; } -static void listdir(struct mg_connection *c, struct mg_http_message *hm, - const struct mg_http_serve_opts *opts, char *dir) { - const char *sort_js_code = - ""; - struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; - struct printdirentrydata d = {c, hm, opts, dir}; - char tmp[10], buf[MG_PATH_MAX]; - size_t off, n; - int len = mg_url_decode(hm->uri.buf, hm->uri.len, buf, sizeof(buf), 0); - struct mg_str uri = len > 0 ? mg_str_n(buf, (size_t) len) : hm->uri; - - mg_printf(c, - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/html; charset=utf-8\r\n" - "%s" - "Content-Length: \r\n\r\n", - opts->extra_headers == NULL ? "" : opts->extra_headers); - off = c->send.len; // Start of body - mg_printf(c, - "Index of %.*s%s%s" - "" - "

Index of %.*s

" - "" - "" - "" - "\n", - (int) uri.len, uri.buf, sort_js_code, sort_js_code2, - c->mgr->directory_listing_css, (int) uri.len, uri.buf); - mg_printf(c, "%s", - " " - "\n"); - - fs->ls(dir, printdirentry, &d); - mg_printf(c, - "" - "
Name" - "ModifiedSize
..[DIR]
%s
\n", - c->mgr->product_name); - n = mg_snprintf(tmp, sizeof(tmp), "%lu", (unsigned long) (c->send.len - off)); - if (n > sizeof(tmp)) n = 0; - memcpy(c->send.buf + off - 12, tmp, n); // Set content length - c->is_resp = 0; // Mark response end +char *mg_json_get_b64(struct mg_str json, const char *path, int *slen) { + char *result = NULL; + int len = 0, off = mg_json_get(json, path, &len); + if (off >= 0 && json.buf[off] == '"' && len > 1 && + (result = (char *) calloc(1, (size_t) len)) != NULL) { + size_t k = mg_base64_decode(json.buf + off + 1, (size_t) (len - 2), result, + (size_t) len); + if (slen != NULL) *slen = (int) k; + } + return result; } -#endif -// Resolve requested file into `path` and return its fs->st() result -static int uri_to_path2(struct mg_connection *c, struct mg_http_message *hm, - struct mg_fs *fs, struct mg_str url, struct mg_str dir, - char *path, size_t path_size) { - int flags, tmp; - // Append URI to the root_dir, and sanitize it - size_t n = mg_snprintf(path, path_size, "%.*s", (int) dir.len, dir.buf); - if (n + 2 >= path_size) { - mg_http_reply(c, 400, "", "Exceeded path size"); - return -1; - } - path[path_size - 1] = '\0'; - // Terminate root dir with slash - if (n > 0 && path[n - 1] != '/') path[n++] = '/', path[n] = '\0'; - if (url.len < hm->uri.len) { - mg_url_decode(hm->uri.buf + url.len, hm->uri.len - url.len, path + n, - path_size - n, 0); - } - path[path_size - 1] = '\0'; // Double-check - if (!mg_path_is_sane(mg_str_n(path, path_size))) { - mg_http_reply(c, 400, "", "Invalid path"); - return -1; - } - n = strlen(path); - while (n > 1 && path[n - 1] == '/') path[--n] = 0; // Trim trailing slashes - flags = mg_strcmp(hm->uri, mg_str("/")) == 0 ? MG_FS_DIR - : fs->st(path, NULL, NULL); - MG_VERBOSE(("%lu %.*s -> %s %d", c->id, (int) hm->uri.len, hm->uri.buf, path, - flags)); - if (flags == 0) { - // Do nothing - let's caller decide - } else if ((flags & MG_FS_DIR) && hm->uri.len > 0 && - hm->uri.buf[hm->uri.len - 1] != '/') { - mg_printf(c, - "HTTP/1.1 301 Moved\r\n" - "Location: %.*s/\r\n" - "Content-Length: 0\r\n" - "\r\n", - (int) hm->uri.len, hm->uri.buf); - c->is_resp = 0; - flags = -1; - } else if (flags & MG_FS_DIR) { - if (((mg_snprintf(path + n, path_size - n, "/" MG_HTTP_INDEX) > 0 && - (tmp = fs->st(path, NULL, NULL)) != 0) || - (mg_snprintf(path + n, path_size - n, "/index.shtml") > 0 && - (tmp = fs->st(path, NULL, NULL)) != 0))) { - flags = tmp; - } else if ((mg_snprintf(path + n, path_size - n, "/" MG_HTTP_INDEX ".gz") > - 0 && - (tmp = fs->st(path, NULL, NULL)) != - 0)) { // check for gzipped index - flags = tmp; - path[n + 1 + strlen(MG_HTTP_INDEX)] = - '\0'; // Remove appended .gz in index file name - } else { - path[n] = '\0'; // Remove appended index file name +char *mg_json_get_hex(struct mg_str json, const char *path, int *slen) { + char *result = NULL; + int len = 0, off = mg_json_get(json, path, &len); + if (off >= 0 && json.buf[off] == '"' && len > 1 && + (result = (char *) calloc(1, (size_t) len / 2)) != NULL) { + int i; + for (i = 0; i < len - 2; i += 2) { + mg_str_to_num(mg_str_n(json.buf + off + 1 + i, 2), 16, &result[i >> 1], + sizeof(uint8_t)); } + result[len / 2 - 1] = '\0'; + if (slen != NULL) *slen = len / 2 - 1; } - return flags; + return result; } -static int uri_to_path(struct mg_connection *c, struct mg_http_message *hm, - const struct mg_http_serve_opts *opts, char *path, - size_t path_size) { - struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; - struct mg_str k, v, part, s = mg_str(opts->root_dir), u = {NULL, 0}, p = u; - while (mg_span(s, &part, &s, ',')) { - if (!mg_span(part, &k, &v, '=')) k = part, v = mg_str_n(NULL, 0); - if (v.len == 0) v = k, k = mg_str("/"), u = k, p = v; - if (hm->uri.len < k.len) continue; - if (mg_strcmp(k, mg_str_n(hm->uri.buf, k.len)) != 0) continue; - u = k, p = v; - } - return uri_to_path2(c, hm, fs, u, p, path, path_size); +long mg_json_get_long(struct mg_str json, const char *path, long dflt) { + double dv; + long result = dflt; + if (mg_json_get_num(json, path, &dv)) result = (long) dv; + return result; } -void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm, - const struct mg_http_serve_opts *opts) { - char path[MG_PATH_MAX]; - const char *sp = opts->ssi_pattern; - int flags = uri_to_path(c, hm, opts, path, sizeof(path)); - if (flags < 0) { - // Do nothing: the response has already been sent by uri_to_path() - } else if (flags & MG_FS_DIR) { -#if MG_ENABLE_DIRLIST - listdir(c, hm, opts, path); -#else - mg_http_reply(c, 403, "", "Forbidden\n"); +#ifdef MG_ENABLE_LINES +#line 1 "src/log.c" #endif - } else if (flags && sp != NULL && mg_match(mg_str(path), mg_str(sp), NULL)) { - mg_http_serve_ssi(c, opts->root_dir, path); - } else { - mg_http_serve_file(c, hm, path, opts); - } + + + + + +int mg_log_level = MG_LL_INFO; +static mg_pfn_t s_log_func = mg_pfn_stdout; +static void *s_log_func_param = NULL; + +void mg_log_set_fn(mg_pfn_t fn, void *param) { + s_log_func = fn; + s_log_func_param = param; } -static bool mg_is_url_safe(int c) { - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || c == '.' || c == '_' || c == '-' || c == '~'; +static void logc(unsigned char c) { + s_log_func((char) c, s_log_func_param); } -size_t mg_url_encode(const char *s, size_t sl, char *buf, size_t len) { - size_t i, n = 0; - for (i = 0; i < sl; i++) { - int c = *(unsigned char *) &s[i]; - if (n + 4 >= len) return 0; - if (mg_is_url_safe(c)) { - buf[n++] = s[i]; - } else { - mg_snprintf(&buf[n], 4, "%%%M", mg_print_hex, 1, &s[i]); - n += 3; - } - } - if (len > 0 && n < len - 1) buf[n] = '\0'; // Null-terminate the destination - if (len > 0) buf[len - 1] = '\0'; // Always. - return n; +static void logs(const char *buf, size_t len) { + size_t i; + for (i = 0; i < len; i++) logc(((unsigned char *) buf)[i]); } -void mg_http_creds(struct mg_http_message *hm, char *user, size_t userlen, - char *pass, size_t passlen) { - struct mg_str *v = mg_http_get_header(hm, "Authorization"); - user[0] = pass[0] = '\0'; - if (v != NULL && v->len > 6 && memcmp(v->buf, "Basic ", 6) == 0) { - char buf[256]; - size_t n = mg_base64_decode(v->buf + 6, v->len - 6, buf, sizeof(buf)); - const char *p = (const char *) memchr(buf, ':', n > 0 ? n : 0); - if (p != NULL) { - mg_snprintf(user, userlen, "%.*s", p - buf, buf); - mg_snprintf(pass, passlen, "%.*s", n - (size_t) (p - buf) - 1, p + 1); - } - } else if (v != NULL && v->len > 7 && memcmp(v->buf, "Bearer ", 7) == 0) { - mg_snprintf(pass, passlen, "%.*s", (int) v->len - 7, v->buf + 7); - } else if ((v = mg_http_get_header(hm, "Cookie")) != NULL) { - struct mg_str t = mg_http_get_header_var(*v, mg_str_n("access_token", 12)); - if (t.len > 0) mg_snprintf(pass, passlen, "%.*s", (int) t.len, t.buf); - } else { - mg_http_get_var(&hm->query, "access_token", pass, passlen); - } +#if MG_ENABLE_CUSTOM_LOG +// Let user define their own mg_log_prefix() and mg_log() +#else +void mg_log_prefix(int level, const char *file, int line, const char *fname) { + const char *p = strrchr(file, '/'); + char buf[41]; + size_t n; + if (p == NULL) p = strrchr(file, '\\'); + n = mg_snprintf(buf, sizeof(buf), "%-6llx %d %s:%d:%s", mg_millis(), level, + p == NULL ? file : p + 1, line, fname); + if (n > sizeof(buf) - 2) n = sizeof(buf) - 2; + while (n < sizeof(buf)) buf[n++] = ' '; + logs(buf, n - 1); } -static struct mg_str stripquotes(struct mg_str s) { - return s.len > 1 && s.buf[0] == '"' && s.buf[s.len - 1] == '"' - ? mg_str_n(s.buf + 1, s.len - 2) - : s; +void mg_log(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + mg_vxprintf(s_log_func, s_log_func_param, fmt, &ap); + va_end(ap); + logs("\r\n", 2); +} +#endif + +static unsigned char nibble(unsigned c) { + return (unsigned char) (c < 10 ? c + '0' : c + 'W'); } -struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v) { +#define ISPRINT(x) ((x) >= ' ' && (x) <= '~') +void mg_hexdump(const void *buf, size_t len) { + const unsigned char *p = (const unsigned char *) buf; + unsigned char ascii[16], alen = 0; size_t i; - for (i = 0; v.len > 0 && i + v.len + 2 < s.len; i++) { - if (s.buf[i + v.len] == '=' && memcmp(&s.buf[i], v.buf, v.len) == 0) { - const char *p = &s.buf[i + v.len + 1], *b = p, *x = &s.buf[s.len]; - int q = p < x && *p == '"' ? 1 : 0; - while (p < x && - (q ? p == b || *p != '"' : *p != ';' && *p != ' ' && *p != ',')) - p++; - // MG_INFO(("[%.*s] [%.*s] [%.*s]", (int) s.len, s.buf, (int) v.len, - // v.buf, (int) (p - b), b)); - return stripquotes(mg_str_n(b, (size_t) (p - b + q))); + for (i = 0; i < len; i++) { + if ((i % 16) == 0) { + // Print buffered ascii chars + if (i > 0) logs(" ", 2), logs((char *) ascii, 16), logc('\n'), alen = 0; + // Print hex address, then \t + logc(nibble((i >> 12) & 15)), logc(nibble((i >> 8) & 15)), + logc(nibble((i >> 4) & 15)), logc('0'), logs(" ", 3); } + logc(nibble(p[i] >> 4)), logc(nibble(p[i] & 15)); // Two nibbles, e.g. c5 + logc(' '); // Space after hex number + ascii[alen++] = ISPRINT(p[i]) ? p[i] : '.'; // Add to the ascii buf } - return mg_str_n(NULL, 0); + while (alen < 16) logs(" ", 3), ascii[alen++] = ' '; + logs(" ", 2), logs((char *) ascii, 16), logc('\n'); } -long mg_http_upload(struct mg_connection *c, struct mg_http_message *hm, - struct mg_fs *fs, const char *dir, size_t max_size) { - char buf[20] = "0", file[MG_PATH_MAX], path[MG_PATH_MAX]; - long res = 0, offset; - mg_http_get_var(&hm->query, "offset", buf, sizeof(buf)); - mg_http_get_var(&hm->query, "file", file, sizeof(file)); - offset = strtol(buf, NULL, 0); - mg_snprintf(path, sizeof(path), "%s%c%s", dir, MG_DIRSEP, file); - if (hm->body.len == 0) { - mg_http_reply(c, 200, "", "%ld", res); // Nothing to write - } else if (file[0] == '\0') { - mg_http_reply(c, 400, "", "file required"); - res = -1; - } else if (mg_path_is_sane(mg_str(file)) == false) { - mg_http_reply(c, 400, "", "%s: invalid file", file); - res = -2; - } else if (offset < 0) { - mg_http_reply(c, 400, "", "offset required"); - res = -3; - } else if ((size_t) offset + hm->body.len > max_size) { - mg_http_reply(c, 400, "", "%s: over max size of %lu", path, - (unsigned long) max_size); - res = -4; +#ifdef MG_ENABLE_LINES +#line 1 "src/md5.c" +#endif + + + +// This code implements the MD5 message-digest algorithm. +// The algorithm is due to Ron Rivest. This code was +// written by Colin Plumb in 1993, no copyright is claimed. +// This code is in the public domain; do with it what you wish. +// +// Equivalent code is available from RSA Data Security, Inc. +// This code has been tested against that, and is equivalent, +// except that you don't need to include two pages of legalese +// with every copy. +// +// To compute the message digest of a chunk of bytes, declare an +// MD5Context structure, pass it to MD5Init, call MD5Update as +// needed on buffers full of bytes, and then call MD5Final, which +// will fill a supplied 16-byte array with the digest. + +#if defined(MG_ENABLE_MD5) && MG_ENABLE_MD5 + +static void mg_byte_reverse(unsigned char *buf, unsigned longs) { + if (MG_BIG_ENDIAN) { + do { + uint32_t t = (uint32_t) ((unsigned) buf[3] << 8 | buf[2]) << 16 | + ((unsigned) buf[1] << 8 | buf[0]); + *(uint32_t *) buf = t; + buf += 4; + } while (--longs); } else { - struct mg_fd *fd; - size_t current_size = 0; - MG_DEBUG(("%s -> %lu bytes @ %ld", path, hm->body.len, offset)); - if (offset == 0) fs->rm(path); // If offset if 0, truncate file - fs->st(path, ¤t_size, NULL); - if (offset > 0 && current_size != (size_t) offset) { - mg_http_reply(c, 400, "", "%s: offset mismatch", path); - res = -5; - } else if ((fd = mg_fs_open(fs, path, MG_FS_WRITE)) == NULL) { - mg_http_reply(c, 400, "", "open(%s): %d", path, errno); - res = -6; - } else { - res = offset + (long) fs->wr(fd->fd, hm->body.buf, hm->body.len); - mg_fs_close(fd); - mg_http_reply(c, 200, "", "%ld", res); - } + (void) buf, (void) longs; // Little endian. Do nothing } - return res; } -int mg_http_status(const struct mg_http_message *hm) { - return atoi(hm->uri.buf); -} +#define F1(x, y, z) (z ^ (x & (y ^ z))) +#define F2(x, y, z) F1(z, x, y) +#define F3(x, y, z) (x ^ y ^ z) +#define F4(x, y, z) (y ^ (x | ~z)) -static bool is_hex_digit(int c) { - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || - (c >= 'A' && c <= 'F'); -} +#define MD5STEP(f, w, x, y, z, data, s) \ + (w += f(x, y, z) + data, w = w << s | w >> (32 - s), w += x) -static int skip_chunk(const char *buf, int len, int *pl, int *dl) { - int i = 0, n = 0; - if (len < 3) return 0; - while (i < len && is_hex_digit(buf[i])) i++; - if (i == 0) return -1; // Error, no length specified - if (i > (int) sizeof(int) * 2) return -1; // Chunk length is too big - if (len < i + 1 || buf[i] != '\r' || buf[i + 1] != '\n') return -1; // Error - if (mg_str_to_num(mg_str_n(buf, (size_t) i), 16, &n, sizeof(int)) == false) - return -1; // Decode chunk length, overflow - if (n < 0) return -1; // Error. TODO(): some checks now redundant - if (n > len - i - 4) return 0; // Chunk not yet fully buffered - if (buf[i + n + 2] != '\r' || buf[i + n + 3] != '\n') return -1; // Error - *pl = i + 2, *dl = n; - return i + 2 + n + 2; +/* + * Start MD5 accumulation. Set bit count to 0 and buffer to mysterious + * initialization constants. + */ +void mg_md5_init(mg_md5_ctx *ctx) { + ctx->buf[0] = 0x67452301; + ctx->buf[1] = 0xefcdab89; + ctx->buf[2] = 0x98badcfe; + ctx->buf[3] = 0x10325476; + + ctx->bits[0] = 0; + ctx->bits[1] = 0; } -static void http_cb(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_READ || ev == MG_EV_CLOSE || - (ev == MG_EV_POLL && c->is_accepted && !c->is_draining && - c->recv.len > 0)) { // see #2796 - struct mg_http_message hm; - size_t ofs = 0; // Parsing offset - while (c->is_resp == 0 && ofs < c->recv.len) { - const char *buf = (char *) c->recv.buf + ofs; - int n = mg_http_parse(buf, c->recv.len - ofs, &hm); - struct mg_str *te; // Transfer - encoding header - bool is_chunked = false; - size_t old_len = c->recv.len; - if (n < 0) { - // We don't use mg_error() here, to avoid closing pipelined requests - // prematurely, see #2592 - MG_ERROR(("HTTP parse, %lu bytes", c->recv.len)); - c->is_draining = 1; - mg_hexdump(buf, c->recv.len - ofs > 16 ? 16 : c->recv.len - ofs); - c->recv.len = 0; - return; - } - if (n == 0) break; // Request is not buffered yet - mg_call(c, MG_EV_HTTP_HDRS, &hm); // Got all HTTP headers - if (c->recv.len != old_len) { - // User manipulated received data. Wash our hands - MG_DEBUG(("%lu detaching HTTP handler", c->id)); - c->pfn = NULL; - return; - } - if (ev == MG_EV_CLOSE) { // If client did not set Content-Length - hm.message.len = c->recv.len - ofs; // and closes now, deliver MSG - hm.body.len = hm.message.len - (size_t) (hm.body.buf - hm.message.buf); - } - if ((te = mg_http_get_header(&hm, "Transfer-Encoding")) != NULL) { - if (mg_strcasecmp(*te, mg_str("chunked")) == 0) { - is_chunked = true; - } else { - mg_error(c, "Invalid Transfer-Encoding"); // See #2460 - return; - } - } else if (mg_http_get_header(&hm, "Content-length") == NULL) { - // #2593: HTTP packets must contain either Transfer-Encoding or - // Content-length - bool is_response = mg_ncasecmp(hm.method.buf, "HTTP/", 5) == 0; - bool require_content_len = false; - if (!is_response && (mg_strcasecmp(hm.method, mg_str("POST")) == 0 || - mg_strcasecmp(hm.method, mg_str("PUT")) == 0)) { - // POST and PUT should include an entity body. Therefore, they should - // contain a Content-length header. Other requests can also contain a - // body, but their content has no defined semantics (RFC 7231) - require_content_len = true; - ofs += (size_t) n; // this request has been processed - } else if (is_response) { - // HTTP spec 7.2 Entity body: All other responses must include a body - // or Content-Length header field defined with a value of 0. - int status = mg_http_status(&hm); - require_content_len = status >= 200 && status != 204 && status != 304; - } - if (require_content_len) { - mg_http_reply(c, 411, "", ""); - MG_ERROR(("%s", "Content length missing from request")); - } - } +static void mg_md5_transform(uint32_t buf[4], uint32_t const in[16]) { + uint32_t a, b, c, d; - if (is_chunked) { - // For chunked data, strip off prefixes and suffixes from chunks - // and relocate them right after the headers, then report a message - char *s = (char *) c->recv.buf + ofs + n; - int o = 0, pl, dl, cl, len = (int) (c->recv.len - ofs - (size_t) n); + a = buf[0]; + b = buf[1]; + c = buf[2]; + d = buf[3]; - // Find zero-length chunk (the end of the body) - while ((cl = skip_chunk(s + o, len - o, &pl, &dl)) > 0 && dl) o += cl; - if (cl == 0) break; // No zero-len chunk, buffer more data - if (cl < 0) { - mg_error(c, "Invalid chunk"); - break; - } + MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7); + MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12); + MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17); + MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22); + MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7); + MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12); + MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17); + MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22); + MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7); + MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12); + MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17); + MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22); + MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7); + MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12); + MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17); + MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22); - // Zero chunk found. Second pass: strip + relocate - o = 0, hm.body.len = 0, hm.message.len = (size_t) n; - while ((cl = skip_chunk(s + o, len - o, &pl, &dl)) > 0) { - memmove(s + hm.body.len, s + o + pl, (size_t) dl); - o += cl, hm.body.len += (size_t) dl, hm.message.len += (size_t) dl; - if (dl == 0) break; - } - ofs += (size_t) (n + o); - } else { // Normal, non-chunked data - size_t len = c->recv.len - ofs - (size_t) n; - if (hm.body.len > len) break; // Buffer more data - ofs += (size_t) n + hm.body.len; - } + MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5); + MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9); + MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14); + MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20); + MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5); + MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9); + MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14); + MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20); + MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5); + MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9); + MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14); + MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20); + MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5); + MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9); + MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14); + MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20); - if (c->is_accepted) c->is_resp = 1; // Start generating response - mg_call(c, MG_EV_HTTP_MSG, &hm); // User handler can clear is_resp - if (c->is_accepted && !c->is_resp) { - struct mg_str *cc = mg_http_get_header(&hm, "Connection"); - if (cc != NULL && mg_strcasecmp(*cc, mg_str("close")) == 0) { - c->is_draining = 1; // honor "Connection: close" - break; - } - } - } - if (ofs > 0) mg_iobuf_del(&c->recv, 0, ofs); // Delete processed data - } - (void) ev_data; + MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4); + MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11); + MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16); + MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23); + MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4); + MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11); + MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16); + MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23); + MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4); + MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11); + MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16); + MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23); + MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4); + MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11); + MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16); + MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23); + + MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6); + MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10); + MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15); + MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21); + MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6); + MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10); + MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15); + MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21); + MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6); + MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10); + MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15); + MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21); + MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6); + MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10); + MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15); + MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21); + + buf[0] += a; + buf[1] += b; + buf[2] += c; + buf[3] += d; } -static void mg_hfn(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_HTTP_MSG) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - if (mg_match(hm->uri, mg_str("/quit"), NULL)) { - mg_http_reply(c, 200, "", "ok\n"); - c->is_draining = 1; - c->data[0] = 'X'; - } else if (mg_match(hm->uri, mg_str("/debug"), NULL)) { - int level = (int) mg_json_get_long(hm->body, "$.level", MG_LL_DEBUG); - mg_log_set(level); - mg_http_reply(c, 200, "", "Debug level set to %d\n", level); - } else { - mg_http_reply(c, 200, "", "hi\n"); +void mg_md5_update(mg_md5_ctx *ctx, const unsigned char *buf, size_t len) { + uint32_t t; + + t = ctx->bits[0]; + if ((ctx->bits[0] = t + ((uint32_t) len << 3)) < t) ctx->bits[1]++; + ctx->bits[1] += (uint32_t) len >> 29; + + t = (t >> 3) & 0x3f; + + if (t) { + unsigned char *p = (unsigned char *) ctx->in + t; + + t = 64 - t; + if (len < t) { + memcpy(p, buf, len); + return; } - } else if (ev == MG_EV_CLOSE) { - if (c->data[0] == 'X') *(bool *) c->fn_data = true; + memcpy(p, buf, t); + mg_byte_reverse(ctx->in, 16); + mg_md5_transform(ctx->buf, (uint32_t *) ctx->in); + buf += t; + len -= t; } -} -void mg_hello(const char *url) { - struct mg_mgr mgr; - bool done = false; - mg_mgr_init(&mgr); - if (mg_http_listen(&mgr, url, mg_hfn, &done) == NULL) done = true; - while (done == false) mg_mgr_poll(&mgr, 100); - mg_mgr_free(&mgr); -} + while (len >= 64) { + memcpy(ctx->in, buf, 64); + mg_byte_reverse(ctx->in, 16); + mg_md5_transform(ctx->buf, (uint32_t *) ctx->in); + buf += 64; + len -= 64; + } -struct mg_connection *mg_http_connect(struct mg_mgr *mgr, const char *url, - mg_event_handler_t fn, void *fn_data) { - struct mg_connection *c = mg_connect(mgr, url, fn, fn_data); - if (c != NULL) c->pfn = http_cb; - return c; + memcpy(ctx->in, buf, len); } -struct mg_connection *mg_http_listen(struct mg_mgr *mgr, const char *url, - mg_event_handler_t fn, void *fn_data) { - struct mg_connection *c = mg_listen(mgr, url, fn, fn_data); - if (c != NULL) c->pfn = http_cb; - return c; +void mg_md5_final(mg_md5_ctx *ctx, unsigned char digest[16]) { + unsigned count; + unsigned char *p; + uint32_t *a; + + count = (ctx->bits[0] >> 3) & 0x3F; + + p = ctx->in + count; + *p++ = 0x80; + count = 64 - 1 - count; + if (count < 8) { + memset(p, 0, count); + mg_byte_reverse(ctx->in, 16); + mg_md5_transform(ctx->buf, (uint32_t *) ctx->in); + memset(ctx->in, 0, 56); + } else { + memset(p, 0, count - 8); + } + mg_byte_reverse(ctx->in, 14); + + a = (uint32_t *) ctx->in; + a[14] = ctx->bits[0]; + a[15] = ctx->bits[1]; + + mg_md5_transform(ctx->buf, (uint32_t *) ctx->in); + mg_byte_reverse((unsigned char *) ctx->buf, 4); + memcpy(digest, ctx->buf, 16); + memset((char *) ctx, 0, sizeof(*ctx)); } +#endif #ifdef MG_ENABLE_LINES -#line 1 "src/iobuf.c" +#line 1 "src/mqtt.c" #endif -static size_t roundup(size_t size, size_t align) { - return align == 0 ? size : (size + align - 1) / align * align; -} -int mg_iobuf_resize(struct mg_iobuf *io, size_t new_size) { - int ok = 1; - new_size = roundup(new_size, io->align); - if (new_size == 0) { - mg_bzero(io->buf, io->size); - free(io->buf); - io->buf = NULL; - io->len = io->size = 0; - } else if (new_size != io->size) { - // NOTE(lsm): do not use realloc here. Use calloc/free only, to ease the - // porting to some obscure platforms like FreeRTOS - void *p = calloc(1, new_size); - if (p != NULL) { - size_t len = new_size < io->len ? new_size : io->len; - if (len > 0 && io->buf != NULL) memmove(p, io->buf, len); - mg_bzero(io->buf, io->size); - free(io->buf); - io->buf = (unsigned char *) p; - io->size = new_size; - } else { - ok = 0; - MG_ERROR(("%lld->%lld", (uint64_t) io->size, (uint64_t) new_size)); - } - } - return ok; -} -int mg_iobuf_init(struct mg_iobuf *io, size_t size, size_t align) { - io->buf = NULL; - io->align = align; - io->size = io->len = 0; - return mg_iobuf_resize(io, size); -} -size_t mg_iobuf_add(struct mg_iobuf *io, size_t ofs, const void *buf, - size_t len) { - size_t new_size = roundup(io->len + len, io->align); - mg_iobuf_resize(io, new_size); // Attempt to resize - if (new_size != io->size) len = 0; // Resize failure, append nothing - if (ofs < io->len) memmove(io->buf + ofs + len, io->buf + ofs, io->len - ofs); - if (buf != NULL) memmove(io->buf + ofs, buf, len); - if (ofs > io->len) io->len += ofs - io->len; - io->len += len; - return len; -} +#define MQTT_CLEAN_SESSION 0x02 +#define MQTT_HAS_WILL 0x04 +#define MQTT_WILL_RETAIN 0x20 +#define MQTT_HAS_PASSWORD 0x40 +#define MQTT_HAS_USER_NAME 0x80 -size_t mg_iobuf_del(struct mg_iobuf *io, size_t ofs, size_t len) { - if (ofs > io->len) ofs = io->len; - if (ofs + len > io->len) len = io->len - ofs; - if (io->buf) memmove(io->buf + ofs, io->buf + ofs + len, io->len - ofs - len); - if (io->buf) mg_bzero(io->buf + io->len - len, len); - io->len -= len; - return len; +struct mg_mqtt_pmap { + uint8_t id; + uint8_t type; +}; + +static const struct mg_mqtt_pmap s_prop_map[] = { + {MQTT_PROP_PAYLOAD_FORMAT_INDICATOR, MQTT_PROP_TYPE_BYTE}, + {MQTT_PROP_MESSAGE_EXPIRY_INTERVAL, MQTT_PROP_TYPE_INT}, + {MQTT_PROP_CONTENT_TYPE, MQTT_PROP_TYPE_STRING}, + {MQTT_PROP_RESPONSE_TOPIC, MQTT_PROP_TYPE_STRING}, + {MQTT_PROP_CORRELATION_DATA, MQTT_PROP_TYPE_BINARY_DATA}, + {MQTT_PROP_SUBSCRIPTION_IDENTIFIER, MQTT_PROP_TYPE_VARIABLE_INT}, + {MQTT_PROP_SESSION_EXPIRY_INTERVAL, MQTT_PROP_TYPE_INT}, + {MQTT_PROP_ASSIGNED_CLIENT_IDENTIFIER, MQTT_PROP_TYPE_STRING}, + {MQTT_PROP_SERVER_KEEP_ALIVE, MQTT_PROP_TYPE_SHORT}, + {MQTT_PROP_AUTHENTICATION_METHOD, MQTT_PROP_TYPE_STRING}, + {MQTT_PROP_AUTHENTICATION_DATA, MQTT_PROP_TYPE_BINARY_DATA}, + {MQTT_PROP_REQUEST_PROBLEM_INFORMATION, MQTT_PROP_TYPE_BYTE}, + {MQTT_PROP_WILL_DELAY_INTERVAL, MQTT_PROP_TYPE_INT}, + {MQTT_PROP_REQUEST_RESPONSE_INFORMATION, MQTT_PROP_TYPE_BYTE}, + {MQTT_PROP_RESPONSE_INFORMATION, MQTT_PROP_TYPE_STRING}, + {MQTT_PROP_SERVER_REFERENCE, MQTT_PROP_TYPE_STRING}, + {MQTT_PROP_REASON_STRING, MQTT_PROP_TYPE_STRING}, + {MQTT_PROP_RECEIVE_MAXIMUM, MQTT_PROP_TYPE_SHORT}, + {MQTT_PROP_TOPIC_ALIAS_MAXIMUM, MQTT_PROP_TYPE_SHORT}, + {MQTT_PROP_TOPIC_ALIAS, MQTT_PROP_TYPE_SHORT}, + {MQTT_PROP_MAXIMUM_QOS, MQTT_PROP_TYPE_BYTE}, + {MQTT_PROP_RETAIN_AVAILABLE, MQTT_PROP_TYPE_BYTE}, + {MQTT_PROP_USER_PROPERTY, MQTT_PROP_TYPE_STRING_PAIR}, + {MQTT_PROP_MAXIMUM_PACKET_SIZE, MQTT_PROP_TYPE_INT}, + {MQTT_PROP_WILDCARD_SUBSCRIPTION_AVAILABLE, MQTT_PROP_TYPE_BYTE}, + {MQTT_PROP_SUBSCRIPTION_IDENTIFIER_AVAILABLE, MQTT_PROP_TYPE_BYTE}, + {MQTT_PROP_SHARED_SUBSCRIPTION_AVAILABLE, MQTT_PROP_TYPE_BYTE}}; + +void mg_mqtt_send_header(struct mg_connection *c, uint8_t cmd, uint8_t flags, + uint32_t len) { + uint8_t buf[1 + sizeof(len)], *vlen = &buf[1]; + buf[0] = (uint8_t) ((cmd << 4) | flags); + do { + *vlen = len % 0x80; + len /= 0x80; + if (len > 0) *vlen |= 0x80; + vlen++; + } while (len > 0 && vlen < &buf[sizeof(buf)]); + mg_send(c, buf, (size_t) (vlen - buf)); } -void mg_iobuf_free(struct mg_iobuf *io) { - mg_iobuf_resize(io, 0); +static void mg_send_u16(struct mg_connection *c, uint16_t value) { + mg_send(c, &value, sizeof(value)); } -#ifdef MG_ENABLE_LINES -#line 1 "src/json.c" -#endif +static void mg_send_u32(struct mg_connection *c, uint32_t value) { + mg_send(c, &value, sizeof(value)); +} +static uint8_t varint_size(size_t length) { + uint8_t bytes_needed = 0; + do { + bytes_needed++; + length /= 0x80; + } while (length > 0); + return bytes_needed; +} +static size_t encode_varint(uint8_t *buf, size_t value) { + size_t len = 0; + do { + uint8_t b = (uint8_t) (value % 128); + value /= 128; + if (value > 0) b |= 0x80; + buf[len++] = b; + } while (value > 0); -static const char *escapeseq(int esc) { - return esc ? "\b\f\n\r\t\\\"" : "bfnrt\\\""; + return len; } -static char json_esc(int c, int esc) { - const char *p, *esc1 = escapeseq(esc), *esc2 = escapeseq(!esc); - for (p = esc1; *p != '\0'; p++) { - if (*p == c) return esc2[p - esc1]; +static size_t decode_varint(const uint8_t *buf, size_t len, size_t *value) { + size_t multiplier = 1, offset; + *value = 0; + + for (offset = 0; offset < 4 && offset < len; offset++) { + uint8_t encoded_byte = buf[offset]; + *value += (encoded_byte & 0x7f) * multiplier; + multiplier *= 128; + + if ((encoded_byte & 0x80) == 0) return offset + 1; } + return 0; } -static int mg_pass_string(const char *s, int len) { - int i; - for (i = 0; i < len; i++) { - if (s[i] == '\\' && i + 1 < len && json_esc(s[i + 1], 1)) { - i++; - } else if (s[i] == '\0') { - return MG_JSON_INVALID; - } else if (s[i] == '"') { - return i; - } +static int mqtt_prop_type_by_id(uint8_t prop_id) { + size_t i, num_properties = sizeof(s_prop_map) / sizeof(s_prop_map[0]); + for (i = 0; i < num_properties; ++i) { + if (s_prop_map[i].id == prop_id) return s_prop_map[i].type; } - return MG_JSON_INVALID; + return -1; // Property ID not found } -static double mg_atod(const char *p, int len, int *numlen) { - double d = 0.0; - int i = 0, sign = 1; - - // Sign - if (i < len && *p == '-') { - sign = -1, i++; - } else if (i < len && *p == '+') { - i++; +// Returns the size of the properties section, without the +// size of the content's length +static size_t get_properties_length(struct mg_mqtt_prop *props, size_t count) { + size_t i, size = 0; + for (i = 0; i < count; i++) { + size++; // identifier + switch (mqtt_prop_type_by_id(props[i].id)) { + case MQTT_PROP_TYPE_STRING_PAIR: + size += (uint32_t) (props[i].val.len + props[i].key.len + + 2 * sizeof(uint16_t)); + break; + case MQTT_PROP_TYPE_STRING: + size += (uint32_t) (props[i].val.len + sizeof(uint16_t)); + break; + case MQTT_PROP_TYPE_BINARY_DATA: + size += (uint32_t) (props[i].val.len + sizeof(uint16_t)); + break; + case MQTT_PROP_TYPE_VARIABLE_INT: + size += varint_size((uint32_t) props[i].iv); + break; + case MQTT_PROP_TYPE_INT: + size += (uint32_t) sizeof(uint32_t); + break; + case MQTT_PROP_TYPE_SHORT: + size += (uint32_t) sizeof(uint16_t); + break; + case MQTT_PROP_TYPE_BYTE: + size += (uint32_t) sizeof(uint8_t); + break; + default: + return size; // cannot parse further down + } } - // Decimal - for (; i < len && p[i] >= '0' && p[i] <= '9'; i++) { - d *= 10.0; - d += p[i] - '0'; - } - d *= sign; + return size; +} - // Fractional - if (i < len && p[i] == '.') { - double frac = 0.0, base = 0.1; - i++; - for (; i < len && p[i] >= '0' && p[i] <= '9'; i++) { - frac += base * (p[i] - '0'); - base /= 10.0; +// returns the entire size of the properties section, including the +// size of the variable length of the content +static size_t get_props_size(struct mg_mqtt_prop *props, size_t count) { + size_t size = get_properties_length(props, count); + size += varint_size(size); + return size; +} + +static void mg_send_mqtt_properties(struct mg_connection *c, + struct mg_mqtt_prop *props, size_t nprops) { + size_t total_size = get_properties_length(props, nprops); + uint8_t buf_v[4] = {0, 0, 0, 0}; + uint8_t buf[4] = {0, 0, 0, 0}; + size_t i, len = encode_varint(buf, total_size); + + mg_send(c, buf, (size_t) len); + for (i = 0; i < nprops; i++) { + mg_send(c, &props[i].id, sizeof(props[i].id)); + switch (mqtt_prop_type_by_id(props[i].id)) { + case MQTT_PROP_TYPE_STRING_PAIR: + mg_send_u16(c, mg_htons((uint16_t) props[i].key.len)); + mg_send(c, props[i].key.buf, props[i].key.len); + mg_send_u16(c, mg_htons((uint16_t) props[i].val.len)); + mg_send(c, props[i].val.buf, props[i].val.len); + break; + case MQTT_PROP_TYPE_BYTE: + mg_send(c, &props[i].iv, sizeof(uint8_t)); + break; + case MQTT_PROP_TYPE_SHORT: + mg_send_u16(c, mg_htons((uint16_t) props[i].iv)); + break; + case MQTT_PROP_TYPE_INT: + mg_send_u32(c, mg_htonl((uint32_t) props[i].iv)); + break; + case MQTT_PROP_TYPE_STRING: + mg_send_u16(c, mg_htons((uint16_t) props[i].val.len)); + mg_send(c, props[i].val.buf, props[i].val.len); + break; + case MQTT_PROP_TYPE_BINARY_DATA: + mg_send_u16(c, mg_htons((uint16_t) props[i].val.len)); + mg_send(c, props[i].val.buf, props[i].val.len); + break; + case MQTT_PROP_TYPE_VARIABLE_INT: + len = encode_varint(buf_v, props[i].iv); + mg_send(c, buf_v, (size_t) len); + break; } - d += frac * sign; } +} - // Exponential - if (i < len && (p[i] == 'e' || p[i] == 'E')) { - int j, exp = 0, minus = 0; - i++; - if (i < len && p[i] == '-') minus = 1, i++; - if (i < len && p[i] == '+') i++; - while (i < len && p[i] >= '0' && p[i] <= '9' && exp < 308) - exp = exp * 10 + (p[i++] - '0'); - if (minus) exp = -exp; - for (j = 0; j < exp; j++) d *= 10.0; - for (j = 0; j < -exp; j++) d /= 10.0; - } +size_t mg_mqtt_next_prop(struct mg_mqtt_message *msg, struct mg_mqtt_prop *prop, + size_t ofs) { + uint8_t *i = (uint8_t *) msg->dgram.buf + msg->props_start + ofs; + uint8_t *end = (uint8_t *) msg->dgram.buf + msg->dgram.len; + size_t new_pos = ofs, len; + prop->id = i[0]; - if (numlen != NULL) *numlen = i; - return d; -} + if (ofs >= msg->dgram.len || ofs >= msg->props_start + msg->props_size) + return 0; + i++, new_pos++; -// Iterate over object or array elements -size_t mg_json_next(struct mg_str obj, size_t ofs, struct mg_str *key, - struct mg_str *val) { - if (ofs >= obj.len) { - ofs = 0; // Out of boundaries, stop scanning - } else if (obj.len < 2 || (*obj.buf != '{' && *obj.buf != '[')) { - ofs = 0; // Not an array or object, stop - } else { - struct mg_str sub = mg_str_n(obj.buf + ofs, obj.len - ofs); - if (ofs == 0) ofs++, sub.buf++, sub.len--; - if (*obj.buf == '[') { // Iterate over an array - int n = 0, o = mg_json_get(sub, "$", &n); - if (n < 0 || o < 0 || (size_t) (o + n) > sub.len) { - ofs = 0; // Error parsing key, stop scanning - } else { - if (key) *key = mg_str_n(NULL, 0); - if (val) *val = mg_str_n(sub.buf + o, (size_t) n); - ofs = (size_t) (&sub.buf[o + n] - obj.buf); - } - } else { // Iterate over an object - int n = 0, o = mg_json_get(sub, "$", &n); - if (n < 0 || o < 0 || (size_t) (o + n) > sub.len) { - ofs = 0; // Error parsing key, stop scanning - } else { - if (key) *key = mg_str_n(sub.buf + o, (size_t) n); - sub.buf += o + n, sub.len -= (size_t) (o + n); - while (sub.len > 0 && *sub.buf != ':') sub.len--, sub.buf++; - if (sub.len > 0 && *sub.buf == ':') sub.len--, sub.buf++; - n = 0, o = mg_json_get(sub, "$", &n); - if (n < 0 || o < 0 || (size_t) (o + n) > sub.len) { - ofs = 0; // Error parsing value, stop scanning - } else { - if (val) *val = mg_str_n(sub.buf + o, (size_t) n); - ofs = (size_t) (&sub.buf[o + n] - obj.buf); - } - } - } - // MG_INFO(("SUB ofs %u %.*s", ofs, sub.len, sub.buf)); - while (ofs && ofs < obj.len && - (obj.buf[ofs] == ' ' || obj.buf[ofs] == '\t' || - obj.buf[ofs] == '\n' || obj.buf[ofs] == '\r')) { - ofs++; - } - if (ofs && ofs < obj.len && obj.buf[ofs] == ',') ofs++; - if (ofs > obj.len) ofs = 0; + switch (mqtt_prop_type_by_id(prop->id)) { + case MQTT_PROP_TYPE_STRING_PAIR: + prop->key.len = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); + prop->key.buf = (char *) i + 2; + i += 2 + prop->key.len; + prop->val.len = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); + prop->val.buf = (char *) i + 2; + new_pos += 2 * sizeof(uint16_t) + prop->val.len + prop->key.len; + break; + case MQTT_PROP_TYPE_BYTE: + prop->iv = (uint8_t) i[0]; + new_pos++; + break; + case MQTT_PROP_TYPE_SHORT: + prop->iv = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); + new_pos += sizeof(uint16_t); + break; + case MQTT_PROP_TYPE_INT: + prop->iv = ((uint32_t) i[0] << 24) | ((uint32_t) i[1] << 16) | + ((uint32_t) i[2] << 8) | i[3]; + new_pos += sizeof(uint32_t); + break; + case MQTT_PROP_TYPE_STRING: + prop->val.len = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); + prop->val.buf = (char *) i + 2; + new_pos += 2 + prop->val.len; + break; + case MQTT_PROP_TYPE_BINARY_DATA: + prop->val.len = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); + prop->val.buf = (char *) i + 2; + new_pos += 2 + prop->val.len; + break; + case MQTT_PROP_TYPE_VARIABLE_INT: + len = decode_varint(i, (size_t) (end - i), (size_t *) &prop->iv); + new_pos = (!len) ? 0 : new_pos + len; + break; + default: + new_pos = 0; } - return ofs; + + return new_pos; } -int mg_json_get(struct mg_str json, const char *path, int *toklen) { - const char *s = json.buf; - int len = (int) json.len; - enum { S_VALUE, S_KEY, S_COLON, S_COMMA_OR_EOO } expecting = S_VALUE; - unsigned char nesting[MG_JSON_MAX_DEPTH]; - int i = 0; // Current offset in `s` - int j = 0; // Offset in `s` we're looking for (return value) - int depth = 0; // Current depth (nesting level) - int ed = 0; // Expected depth - int pos = 1; // Current position in `path` - int ci = -1, ei = -1; // Current and expected index in array +void mg_mqtt_login(struct mg_connection *c, const struct mg_mqtt_opts *opts) { + char client_id[21]; + struct mg_str cid = opts->client_id; + size_t total_len = 7 + 1 + 2 + 2; + uint8_t hdr[8] = {0, 4, 'M', 'Q', 'T', 'T', opts->version, 0}; - if (toklen) *toklen = 0; - if (path[0] != '$') return MG_JSON_INVALID; + if (cid.len == 0) { + mg_random_str(client_id, sizeof(client_id) - 1); + client_id[sizeof(client_id) - 1] = '\0'; + cid = mg_str(client_id); + } -#define MG_CHECKRET(x) \ - do { \ - if (depth == ed && path[pos] == '\0' && ci == ei) { \ - if (toklen) *toklen = i - j + 1; \ - return j; \ - } \ - } while (0) + if (hdr[6] == 0) hdr[6] = 4; // If version is not set, use 4 (3.1.1) + c->is_mqtt5 = hdr[6] == 5; // Set version 5 flag + hdr[7] = (uint8_t) ((opts->qos & 3) << 3); // Connection flags + if (opts->user.len > 0) { + total_len += 2 + (uint32_t) opts->user.len; + hdr[7] |= MQTT_HAS_USER_NAME; + } + if (opts->pass.len > 0) { + total_len += 2 + (uint32_t) opts->pass.len; + hdr[7] |= MQTT_HAS_PASSWORD; + } + if (opts->topic.len > 0) { // allow zero-length msgs, message.len is size_t + total_len += 4 + (uint32_t) opts->topic.len + (uint32_t) opts->message.len; + hdr[7] |= MQTT_HAS_WILL; + } + if (opts->clean || cid.len == 0) hdr[7] |= MQTT_CLEAN_SESSION; + if (opts->retain) hdr[7] |= MQTT_WILL_RETAIN; + total_len += (uint32_t) cid.len; + if (c->is_mqtt5) { + total_len += get_props_size(opts->props, opts->num_props); + if (hdr[7] & MQTT_HAS_WILL) + total_len += get_props_size(opts->will_props, opts->num_will_props); + } -// In the ascii table, the distance between `[` and `]` is 2. -// Ditto for `{` and `}`. Hence +2 in the code below. -#define MG_EOO(x) \ - do { \ - if (depth == ed && ci != ei) return MG_JSON_NOT_FOUND; \ - if (c != nesting[depth - 1] + 2) return MG_JSON_INVALID; \ - depth--; \ - MG_CHECKRET(x); \ - } while (0) + mg_mqtt_send_header(c, MQTT_CMD_CONNECT, 0, (uint32_t) total_len); + mg_send(c, hdr, sizeof(hdr)); + // keepalive == 0 means "do not disconnect us!" + mg_send_u16(c, mg_htons((uint16_t) opts->keepalive)); - for (i = 0; i < len; i++) { - unsigned char c = ((unsigned char *) s)[i]; - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue; - switch (expecting) { - case S_VALUE: - // p("V %s [%.*s] %d %d %d %d\n", path, pos, path, depth, ed, ci, ei); - if (depth == ed) j = i; - if (c == '{') { - if (depth >= (int) sizeof(nesting)) return MG_JSON_TOO_DEEP; - if (depth == ed && path[pos] == '.' && ci == ei) { - // If we start the object, reset array indices - ed++, pos++, ci = ei = -1; - } - nesting[depth++] = c; - expecting = S_KEY; - break; - } else if (c == '[') { - if (depth >= (int) sizeof(nesting)) return MG_JSON_TOO_DEEP; - if (depth == ed && path[pos] == '[' && ei == ci) { - ed++, pos++, ci = 0; - for (ei = 0; path[pos] != ']' && path[pos] != '\0'; pos++) { - ei *= 10; - ei += path[pos] - '0'; - } - if (path[pos] != 0) pos++; - } - nesting[depth++] = c; - break; - } else if (c == ']' && depth > 0) { // Empty array - MG_EOO(']'); - } else if (c == 't' && i + 3 < len && memcmp(&s[i], "true", 4) == 0) { - i += 3; - } else if (c == 'n' && i + 3 < len && memcmp(&s[i], "null", 4) == 0) { - i += 3; - } else if (c == 'f' && i + 4 < len && memcmp(&s[i], "false", 5) == 0) { - i += 4; - } else if (c == '-' || ((c >= '0' && c <= '9'))) { - int numlen = 0; - mg_atod(&s[i], len - i, &numlen); - i += numlen - 1; - } else if (c == '"') { - int n = mg_pass_string(&s[i + 1], len - i - 1); - if (n < 0) return n; - i += n + 1; - } else { - return MG_JSON_INVALID; - } - MG_CHECKRET('V'); - if (depth == ed && ei >= 0) ci++; - expecting = S_COMMA_OR_EOO; - break; + if (c->is_mqtt5) mg_send_mqtt_properties(c, opts->props, opts->num_props); - case S_KEY: - if (c == '"') { - int n = mg_pass_string(&s[i + 1], len - i - 1); - if (n < 0) return n; - if (i + 1 + n >= len) return MG_JSON_NOT_FOUND; - if (depth < ed) return MG_JSON_NOT_FOUND; - if (depth == ed && path[pos - 1] != '.') return MG_JSON_NOT_FOUND; - // printf("K %s [%.*s] [%.*s] %d %d %d %d %d\n", path, pos, path, n, - // &s[i + 1], n, depth, ed, ci, ei); - // NOTE(cpq): in the check sequence below is important. - // strncmp() must go first: it fails fast if the remaining length - // of the path is smaller than `n`. - if (depth == ed && path[pos - 1] == '.' && - strncmp(&s[i + 1], &path[pos], (size_t) n) == 0 && - (path[pos + n] == '\0' || path[pos + n] == '.' || - path[pos + n] == '[')) { - pos += n; - } - i += n + 1; - expecting = S_COLON; - } else if (c == '}') { // Empty object - MG_EOO('}'); - expecting = S_COMMA_OR_EOO; - if (depth == ed && ei >= 0) ci++; - } else { - return MG_JSON_INVALID; - } - break; + mg_send_u16(c, mg_htons((uint16_t) cid.len)); + mg_send(c, cid.buf, cid.len); - case S_COLON: - if (c == ':') { - expecting = S_VALUE; - } else { - return MG_JSON_INVALID; - } - break; + if (hdr[7] & MQTT_HAS_WILL) { + if (c->is_mqtt5) + mg_send_mqtt_properties(c, opts->will_props, opts->num_will_props); - case S_COMMA_OR_EOO: - if (depth <= 0) { - return MG_JSON_INVALID; - } else if (c == ',') { - expecting = (nesting[depth - 1] == '{') ? S_KEY : S_VALUE; - } else if (c == ']' || c == '}') { - if (depth == ed && c == '}' && path[pos - 1] == '.') - return MG_JSON_NOT_FOUND; - if (depth == ed && c == ']' && path[pos - 1] == ',') - return MG_JSON_NOT_FOUND; - MG_EOO('O'); - if (depth == ed && ei >= 0) ci++; - } else { - return MG_JSON_INVALID; - } - break; - } + mg_send_u16(c, mg_htons((uint16_t) opts->topic.len)); + mg_send(c, opts->topic.buf, opts->topic.len); + mg_send_u16(c, mg_htons((uint16_t) opts->message.len)); + mg_send(c, opts->message.buf, opts->message.len); } - return MG_JSON_NOT_FOUND; -} - -struct mg_str mg_json_get_tok(struct mg_str json, const char *path) { - int len = 0, ofs = mg_json_get(json, path, &len); - return mg_str_n(ofs < 0 ? NULL : json.buf + ofs, - (size_t) (len < 0 ? 0 : len)); -} - -bool mg_json_get_num(struct mg_str json, const char *path, double *v) { - int n, toklen, found = 0; - if ((n = mg_json_get(json, path, &toklen)) >= 0 && - (json.buf[n] == '-' || (json.buf[n] >= '0' && json.buf[n] <= '9'))) { - if (v != NULL) *v = mg_atod(json.buf + n, toklen, NULL); - found = 1; + if (opts->user.len > 0) { + mg_send_u16(c, mg_htons((uint16_t) opts->user.len)); + mg_send(c, opts->user.buf, opts->user.len); } - return found; -} - -bool mg_json_get_bool(struct mg_str json, const char *path, bool *v) { - int found = 0, off = mg_json_get(json, path, NULL); - if (off >= 0 && (json.buf[off] == 't' || json.buf[off] == 'f')) { - if (v != NULL) *v = json.buf[off] == 't'; - found = 1; + if (opts->pass.len > 0) { + mg_send_u16(c, mg_htons((uint16_t) opts->pass.len)); + mg_send(c, opts->pass.buf, opts->pass.len); } - return found; } -bool mg_json_unescape(struct mg_str s, char *to, size_t n) { - size_t i, j; - for (i = 0, j = 0; i < s.len && j < n; i++, j++) { - if (s.buf[i] == '\\' && i + 5 < s.len && s.buf[i + 1] == 'u') { - // \uXXXX escape. We process simple one-byte chars \u00xx within ASCII - // range. More complex chars would require dragging in a UTF8 library, - // which is too much for us - if (mg_str_to_num(mg_str_n(s.buf + i + 2, 4), 16, &to[j], - sizeof(uint8_t)) == false) - return false; - i += 5; - } else if (s.buf[i] == '\\' && i + 1 < s.len) { - char c = json_esc(s.buf[i + 1], 0); - if (c == 0) return false; - to[j] = c; - i++; - } else { - to[j] = s.buf[i]; - } - } - if (j >= n) return false; - if (n > 0) to[j] = '\0'; - return true; -} +uint16_t mg_mqtt_pub(struct mg_connection *c, const struct mg_mqtt_opts *opts) { + uint16_t id = opts->retransmit_id; + uint8_t flags = (uint8_t) (((opts->qos & 3) << 1) | (opts->retain ? 1 : 0)); + size_t len = 2 + opts->topic.len + opts->message.len; + MG_DEBUG(("%lu [%.*s] -> [%.*s]", c->id, (int) opts->topic.len, + (char *) opts->topic.buf, (int) opts->message.len, + (char *) opts->message.buf)); + if (opts->qos > 0) len += 2; + if (c->is_mqtt5) len += get_props_size(opts->props, opts->num_props); -char *mg_json_get_str(struct mg_str json, const char *path) { - char *result = NULL; - int len = 0, off = mg_json_get(json, path, &len); - if (off >= 0 && len > 1 && json.buf[off] == '"') { - if ((result = (char *) calloc(1, (size_t) len)) != NULL && - !mg_json_unescape(mg_str_n(json.buf + off + 1, (size_t) (len - 2)), - result, (size_t) len)) { - free(result); - result = NULL; + if (opts->qos > 0 && id != 0) flags |= 1 << 3; + mg_mqtt_send_header(c, MQTT_CMD_PUBLISH, flags, (uint32_t) len); + mg_send_u16(c, mg_htons((uint16_t) opts->topic.len)); + mg_send(c, opts->topic.buf, opts->topic.len); + if (opts->qos > 0) { // need to send 'id' field + if (id == 0) { // generate new one if not resending + if (++c->mgr->mqtt_id == 0) ++c->mgr->mqtt_id; + id = c->mgr->mqtt_id; } + mg_send_u16(c, mg_htons(id)); } - return result; -} - -char *mg_json_get_b64(struct mg_str json, const char *path, int *slen) { - char *result = NULL; - int len = 0, off = mg_json_get(json, path, &len); - if (off >= 0 && json.buf[off] == '"' && len > 1 && - (result = (char *) calloc(1, (size_t) len)) != NULL) { - size_t k = mg_base64_decode(json.buf + off + 1, (size_t) (len - 2), result, - (size_t) len); - if (slen != NULL) *slen = (int) k; - } - return result; -} -char *mg_json_get_hex(struct mg_str json, const char *path, int *slen) { - char *result = NULL; - int len = 0, off = mg_json_get(json, path, &len); - if (off >= 0 && json.buf[off] == '"' && len > 1 && - (result = (char *) calloc(1, (size_t) len / 2)) != NULL) { - int i; - for (i = 0; i < len - 2; i += 2) { - mg_str_to_num(mg_str_n(json.buf + off + 1 + i, 2), 16, &result[i >> 1], - sizeof(uint8_t)); - } - result[len / 2 - 1] = '\0'; - if (slen != NULL) *slen = len / 2 - 1; - } - return result; -} + if (c->is_mqtt5) mg_send_mqtt_properties(c, opts->props, opts->num_props); -long mg_json_get_long(struct mg_str json, const char *path, long dflt) { - double dv; - long result = dflt; - if (mg_json_get_num(json, path, &dv)) result = (long) dv; - return result; + if (opts->message.len > 0) mg_send(c, opts->message.buf, opts->message.len); + return id; } -#ifdef MG_ENABLE_LINES -#line 1 "src/log.c" -#endif +void mg_mqtt_sub(struct mg_connection *c, const struct mg_mqtt_opts *opts) { + uint8_t qos_ = opts->qos & 3; + size_t plen = c->is_mqtt5 ? get_props_size(opts->props, opts->num_props) : 0; + size_t len = 2 + opts->topic.len + 2 + 1 + plen; + mg_mqtt_send_header(c, MQTT_CMD_SUBSCRIBE, 2, (uint32_t) len); + if (++c->mgr->mqtt_id == 0) ++c->mgr->mqtt_id; + mg_send_u16(c, mg_htons(c->mgr->mqtt_id)); + if (c->is_mqtt5) mg_send_mqtt_properties(c, opts->props, opts->num_props); + mg_send_u16(c, mg_htons((uint16_t) opts->topic.len)); + mg_send(c, opts->topic.buf, opts->topic.len); + mg_send(c, &qos_, sizeof(qos_)); +} +int mg_mqtt_parse(const uint8_t *buf, size_t len, uint8_t version, + struct mg_mqtt_message *m) { + uint8_t lc = 0, *p, *end; + uint32_t n = 0, len_len = 0; + memset(m, 0, sizeof(*m)); + m->dgram.buf = (char *) buf; + if (len < 2) return MQTT_INCOMPLETE; + m->cmd = (uint8_t) (buf[0] >> 4); + m->qos = (buf[0] >> 1) & 3; -int mg_log_level = MG_LL_INFO; -static mg_pfn_t s_log_func = mg_pfn_stdout; -static void *s_log_func_param = NULL; + n = len_len = 0; + p = (uint8_t *) buf + 1; + while ((size_t) (p - buf) < len) { + lc = *((uint8_t *) p++); + n += (uint32_t) ((lc & 0x7f) << 7 * len_len); + len_len++; + if (!(lc & 0x80)) break; + if (len_len >= 4) return MQTT_MALFORMED; + } + end = p + n; + if ((lc & 0x80) || (end > buf + len)) return MQTT_INCOMPLETE; + m->dgram.len = (size_t) (end - buf); -void mg_log_set_fn(mg_pfn_t fn, void *param) { - s_log_func = fn; - s_log_func_param = param; + switch (m->cmd) { + case MQTT_CMD_CONNACK: + if (end - p < 2) return MQTT_MALFORMED; + m->ack = p[1]; + break; + case MQTT_CMD_PUBACK: + case MQTT_CMD_PUBREC: + case MQTT_CMD_PUBREL: + case MQTT_CMD_PUBCOMP: + case MQTT_CMD_SUBSCRIBE: + case MQTT_CMD_SUBACK: + case MQTT_CMD_UNSUBSCRIBE: + case MQTT_CMD_UNSUBACK: + if (p + 2 > end) return MQTT_MALFORMED; + m->id = (uint16_t) ((((uint16_t) p[0]) << 8) | p[1]); + p += 2; + break; + case MQTT_CMD_PUBLISH: { + if (p + 2 > end) return MQTT_MALFORMED; + m->topic.len = (uint16_t) ((((uint16_t) p[0]) << 8) | p[1]); + m->topic.buf = (char *) p + 2; + p += 2 + m->topic.len; + if (p > end) return MQTT_MALFORMED; + if (m->qos > 0) { + if (p + 2 > end) return MQTT_MALFORMED; + m->id = (uint16_t) ((((uint16_t) p[0]) << 8) | p[1]); + p += 2; + } + if (p > end) return MQTT_MALFORMED; + if (version == 5 && p + 2 < end) { + len_len = + (uint32_t) decode_varint(p, (size_t) (end - p), &m->props_size); + if (!len_len) return MQTT_MALFORMED; + m->props_start = (size_t) (p + len_len - buf); + p += len_len + m->props_size; + } + if (p > end) return MQTT_MALFORMED; + m->data.buf = (char *) p; + m->data.len = (size_t) (end - p); + break; + } + default: + break; + } + return MQTT_OK; } -static void logc(unsigned char c) { - s_log_func((char) c, s_log_func_param); -} +static void mqtt_cb(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_READ) { + for (;;) { + uint8_t version = c->is_mqtt5 ? 5 : 4; + struct mg_mqtt_message mm; + int rc = mg_mqtt_parse(c->recv.buf, c->recv.len, version, &mm); + if (rc == MQTT_MALFORMED) { + MG_ERROR(("%lu MQTT malformed message", c->id)); + c->is_closing = 1; + break; + } else if (rc == MQTT_OK) { + MG_VERBOSE(("%lu MQTT CMD %d len %d [%.*s]", c->id, mm.cmd, + (int) mm.dgram.len, (int) mm.data.len, mm.data.buf)); + switch (mm.cmd) { + case MQTT_CMD_CONNACK: + mg_call(c, MG_EV_MQTT_OPEN, &mm.ack); + if (mm.ack == 0) { + MG_DEBUG(("%lu Connected", c->id)); + } else { + MG_ERROR(("%lu MQTT auth failed, code %d", c->id, mm.ack)); + c->is_closing = 1; + } + break; + case MQTT_CMD_PUBLISH: { + /*MG_DEBUG(("%lu [%.*s] -> [%.*s]", c->id, (int) mm.topic.len, + mm.topic.buf, (int) mm.data.len, mm.data.buf));*/ + if (mm.qos > 0) { + uint16_t id = mg_ntohs(mm.id); + uint32_t remaining_len = sizeof(id); + if (c->is_mqtt5) remaining_len += 2; // 3.4.2 -static void logs(const char *buf, size_t len) { - size_t i; - for (i = 0; i < len; i++) logc(((unsigned char *) buf)[i]); -} + mg_mqtt_send_header( + c, + (uint8_t) (mm.qos == 2 ? MQTT_CMD_PUBREC : MQTT_CMD_PUBACK), + 0, remaining_len); + mg_send(c, &id, sizeof(id)); -#if MG_ENABLE_CUSTOM_LOG -// Let user define their own mg_log_prefix() and mg_log() -#else -void mg_log_prefix(int level, const char *file, int line, const char *fname) { - const char *p = strrchr(file, '/'); - char buf[41]; - size_t n; - if (p == NULL) p = strrchr(file, '\\'); - n = mg_snprintf(buf, sizeof(buf), "%-6llx %d %s:%d:%s", mg_millis(), level, - p == NULL ? file : p + 1, line, fname); - if (n > sizeof(buf) - 2) n = sizeof(buf) - 2; - while (n < sizeof(buf)) buf[n++] = ' '; - logs(buf, n - 1); + if (c->is_mqtt5) { + uint16_t zero = 0; + mg_send(c, &zero, sizeof(zero)); + } + } + mg_call(c, MG_EV_MQTT_MSG, &mm); // let the app handle qos stuff + break; + } + case MQTT_CMD_PUBREC: { // MQTT5: 3.5.2-1 TODO(): variable header rc + uint16_t id = mg_ntohs(mm.id); + uint32_t remaining_len = sizeof(id); // MQTT5 3.6.2-1 + mg_mqtt_send_header(c, MQTT_CMD_PUBREL, 2, remaining_len); + mg_send(c, &id, sizeof(id)); // MQTT5 3.6.1-1, flags = 2 + break; + } + case MQTT_CMD_PUBREL: { // MQTT5: 3.6.2-1 TODO(): variable header rc + uint16_t id = mg_ntohs(mm.id); + uint32_t remaining_len = sizeof(id); // MQTT5 3.7.2-1 + mg_mqtt_send_header(c, MQTT_CMD_PUBCOMP, 0, remaining_len); + mg_send(c, &id, sizeof(id)); + break; + } + } + mg_call(c, MG_EV_MQTT_CMD, &mm); + mg_iobuf_del(&c->recv, 0, mm.dgram.len); + } else { + break; + } + } + } + (void) ev_data; } -void mg_log(const char *fmt, ...) { - va_list ap; - va_start(ap, fmt); - mg_vxprintf(s_log_func, s_log_func_param, fmt, &ap); - va_end(ap); - logs("\r\n", 2); +void mg_mqtt_ping(struct mg_connection *nc) { + mg_mqtt_send_header(nc, MQTT_CMD_PINGREQ, 0, 0); } -#endif -static unsigned char nibble(unsigned c) { - return (unsigned char) (c < 10 ? c + '0' : c + 'W'); +void mg_mqtt_pong(struct mg_connection *nc) { + mg_mqtt_send_header(nc, MQTT_CMD_PINGRESP, 0, 0); } -#define ISPRINT(x) ((x) >= ' ' && (x) <= '~') -void mg_hexdump(const void *buf, size_t len) { - const unsigned char *p = (const unsigned char *) buf; - unsigned char ascii[16], alen = 0; - size_t i; - for (i = 0; i < len; i++) { - if ((i % 16) == 0) { - // Print buffered ascii chars - if (i > 0) logs(" ", 2), logs((char *) ascii, 16), logc('\n'), alen = 0; - // Print hex address, then \t - logc(nibble((i >> 12) & 15)), logc(nibble((i >> 8) & 15)), - logc(nibble((i >> 4) & 15)), logc('0'), logs(" ", 3); - } - logc(nibble(p[i] >> 4)), logc(nibble(p[i] & 15)); // Two nibbles, e.g. c5 - logc(' '); // Space after hex number - ascii[alen++] = ISPRINT(p[i]) ? p[i] : '.'; // Add to the ascii buf +void mg_mqtt_disconnect(struct mg_connection *c, + const struct mg_mqtt_opts *opts) { + size_t len = 0; + if (c->is_mqtt5) len = 1 + get_props_size(opts->props, opts->num_props); + mg_mqtt_send_header(c, MQTT_CMD_DISCONNECT, 0, (uint32_t) len); + + if (c->is_mqtt5) { + uint8_t zero = 0; + mg_send(c, &zero, sizeof(zero)); // reason code + mg_send_mqtt_properties(c, opts->props, opts->num_props); } - while (alen < 16) logs(" ", 3), ascii[alen++] = ' '; - logs(" ", 2), logs((char *) ascii, 16), logc('\n'); } -#ifdef MG_ENABLE_LINES -#line 1 "src/md5.c" -#endif - - - -// This code implements the MD5 message-digest algorithm. -// The algorithm is due to Ron Rivest. This code was -// written by Colin Plumb in 1993, no copyright is claimed. -// This code is in the public domain; do with it what you wish. -// -// Equivalent code is available from RSA Data Security, Inc. -// This code has been tested against that, and is equivalent, -// except that you don't need to include two pages of legalese -// with every copy. -// -// To compute the message digest of a chunk of bytes, declare an -// MD5Context structure, pass it to MD5Init, call MD5Update as -// needed on buffers full of bytes, and then call MD5Final, which -// will fill a supplied 16-byte array with the digest. - -#if defined(MG_ENABLE_MD5) && MG_ENABLE_MD5 - -static void mg_byte_reverse(unsigned char *buf, unsigned longs) { - if (MG_BIG_ENDIAN) { - do { - uint32_t t = (uint32_t) ((unsigned) buf[3] << 8 | buf[2]) << 16 | - ((unsigned) buf[1] << 8 | buf[0]); - *(uint32_t *) buf = t; - buf += 4; - } while (--longs); - } else { - (void) buf, (void) longs; // Little endian. Do nothing +struct mg_connection *mg_mqtt_connect(struct mg_mgr *mgr, const char *url, + const struct mg_mqtt_opts *opts, + mg_event_handler_t fn, void *fn_data) { + struct mg_connection *c = mg_connect(mgr, url, fn, fn_data); + if (c != NULL) { + struct mg_mqtt_opts empty; + memset(&empty, 0, sizeof(empty)); + mg_mqtt_login(c, opts == NULL ? &empty : opts); + c->pfn = mqtt_cb; } + return c; } -#define F1(x, y, z) (z ^ (x & (y ^ z))) -#define F2(x, y, z) F1(z, x, y) -#define F3(x, y, z) (x ^ y ^ z) -#define F4(x, y, z) (y ^ (x | ~z)) +struct mg_connection *mg_mqtt_listen(struct mg_mgr *mgr, const char *url, + mg_event_handler_t fn, void *fn_data) { + struct mg_connection *c = mg_listen(mgr, url, fn, fn_data); + if (c != NULL) c->pfn = mqtt_cb, c->pfn_data = mgr; + return c; +} -#define MD5STEP(f, w, x, y, z, data, s) \ - (w += f(x, y, z) + data, w = w << s | w >> (32 - s), w += x) +#ifdef MG_ENABLE_LINES +#line 1 "src/net.c" +#endif -/* - * Start MD5 accumulation. Set bit count to 0 and buffer to mysterious - * initialization constants. - */ -void mg_md5_init(mg_md5_ctx *ctx) { - ctx->buf[0] = 0x67452301; - ctx->buf[1] = 0xefcdab89; - ctx->buf[2] = 0x98badcfe; - ctx->buf[3] = 0x10325476; - ctx->bits[0] = 0; - ctx->bits[1] = 0; -} -static void mg_md5_transform(uint32_t buf[4], uint32_t const in[16]) { - uint32_t a, b, c, d; - a = buf[0]; - b = buf[1]; - c = buf[2]; - d = buf[3]; - MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7); - MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12); - MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17); - MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22); - MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7); - MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12); - MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17); - MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22); - MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7); - MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12); - MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17); - MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22); - MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7); - MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12); - MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17); - MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22); - MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5); - MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9); - MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14); - MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20); - MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5); - MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9); - MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14); - MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20); - MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5); - MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9); - MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14); - MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20); - MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5); - MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9); - MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14); - MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20); - MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4); - MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11); - MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16); - MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23); - MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4); - MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11); - MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16); - MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23); - MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4); - MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11); - MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16); - MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23); - MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4); - MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11); - MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16); - MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23); - MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6); - MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10); - MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15); - MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21); - MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6); - MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10); - MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15); - MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21); - MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6); - MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10); - MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15); - MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21); - MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6); - MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10); - MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15); - MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21); - buf[0] += a; - buf[1] += b; - buf[2] += c; - buf[3] += d; +size_t mg_vprintf(struct mg_connection *c, const char *fmt, va_list *ap) { + size_t old = c->send.len; + mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, ap); + return c->send.len - old; } -void mg_md5_update(mg_md5_ctx *ctx, const unsigned char *buf, size_t len) { - uint32_t t; - - t = ctx->bits[0]; - if ((ctx->bits[0] = t + ((uint32_t) len << 3)) < t) ctx->bits[1]++; - ctx->bits[1] += (uint32_t) len >> 29; +size_t mg_printf(struct mg_connection *c, const char *fmt, ...) { + size_t len = 0; + va_list ap; + va_start(ap, fmt); + len = mg_vprintf(c, fmt, &ap); + va_end(ap); + return len; +} - t = (t >> 3) & 0x3f; +static bool mg_atonl(struct mg_str str, struct mg_addr *addr) { + uint32_t localhost = mg_htonl(0x7f000001); + if (mg_strcasecmp(str, mg_str("localhost")) != 0) return false; + memcpy(addr->ip, &localhost, sizeof(uint32_t)); + addr->is_ip6 = false; + return true; +} - if (t) { - unsigned char *p = (unsigned char *) ctx->in + t; +static bool mg_atone(struct mg_str str, struct mg_addr *addr) { + if (str.len > 0) return false; + memset(addr->ip, 0, sizeof(addr->ip)); + addr->is_ip6 = false; + return true; +} - t = 64 - t; - if (len < t) { - memcpy(p, buf, len); - return; +static bool mg_aton4(struct mg_str str, struct mg_addr *addr) { + uint8_t data[4] = {0, 0, 0, 0}; + size_t i, num_dots = 0; + for (i = 0; i < str.len; i++) { + if (str.buf[i] >= '0' && str.buf[i] <= '9') { + int octet = data[num_dots] * 10 + (str.buf[i] - '0'); + if (octet > 255) return false; + data[num_dots] = (uint8_t) octet; + } else if (str.buf[i] == '.') { + if (num_dots >= 3 || i == 0 || str.buf[i - 1] == '.') return false; + num_dots++; + } else { + return false; } - memcpy(p, buf, t); - mg_byte_reverse(ctx->in, 16); - mg_md5_transform(ctx->buf, (uint32_t *) ctx->in); - buf += t; - len -= t; } + if (num_dots != 3 || str.buf[i - 1] == '.') return false; + memcpy(&addr->ip, data, sizeof(data)); + addr->is_ip6 = false; + return true; +} - while (len >= 64) { - memcpy(ctx->in, buf, 64); - mg_byte_reverse(ctx->in, 16); - mg_md5_transform(ctx->buf, (uint32_t *) ctx->in); - buf += 64; - len -= 64; +static bool mg_v4mapped(struct mg_str str, struct mg_addr *addr) { + int i; + uint32_t ipv4; + if (str.len < 14) return false; + if (str.buf[0] != ':' || str.buf[1] != ':' || str.buf[6] != ':') return false; + for (i = 2; i < 6; i++) { + if (str.buf[i] != 'f' && str.buf[i] != 'F') return false; } - - memcpy(ctx->in, buf, len); + // struct mg_str s = mg_str_n(&str.buf[7], str.len - 7); + if (!mg_aton4(mg_str_n(&str.buf[7], str.len - 7), addr)) return false; + memcpy(&ipv4, addr->ip, sizeof(ipv4)); + memset(addr->ip, 0, sizeof(addr->ip)); + addr->ip[10] = addr->ip[11] = 255; + memcpy(&addr->ip[12], &ipv4, 4); + addr->is_ip6 = true; + return true; } -void mg_md5_final(mg_md5_ctx *ctx, unsigned char digest[16]) { - unsigned count; - unsigned char *p; - uint32_t *a; - - count = (ctx->bits[0] >> 3) & 0x3F; - - p = ctx->in + count; - *p++ = 0x80; - count = 64 - 1 - count; - if (count < 8) { - memset(p, 0, count); - mg_byte_reverse(ctx->in, 16); - mg_md5_transform(ctx->buf, (uint32_t *) ctx->in); - memset(ctx->in, 0, 56); - } else { - memset(p, 0, count - 8); +static bool mg_aton6(struct mg_str str, struct mg_addr *addr) { + size_t i, j = 0, n = 0, dc = 42; + addr->scope_id = 0; + if (str.len > 2 && str.buf[0] == '[') str.buf++, str.len -= 2; + if (mg_v4mapped(str, addr)) return true; + for (i = 0; i < str.len; i++) { + if ((str.buf[i] >= '0' && str.buf[i] <= '9') || + (str.buf[i] >= 'a' && str.buf[i] <= 'f') || + (str.buf[i] >= 'A' && str.buf[i] <= 'F')) { + unsigned long val = 0; // TODO(): This loops on chars, refactor + if (i > j + 3) return false; + // MG_DEBUG(("%lu %lu [%.*s]", i, j, (int) (i - j + 1), &str.buf[j])); + mg_str_to_num(mg_str_n(&str.buf[j], i - j + 1), 16, &val, sizeof(val)); + addr->ip[n] = (uint8_t) ((val >> 8) & 255); + addr->ip[n + 1] = (uint8_t) (val & 255); + } else if (str.buf[i] == ':') { + j = i + 1; + if (i > 0 && str.buf[i - 1] == ':') { + dc = n; // Double colon + if (i > 1 && str.buf[i - 2] == ':') return false; + } else if (i > 0) { + n += 2; + } + if (n > 14) return false; + addr->ip[n] = addr->ip[n + 1] = 0; // For trailing :: + } else if (str.buf[i] == '%') { // Scope ID, last in string + return mg_str_to_num(mg_str_n(&str.buf[i + 1], str.len - i - 1), 10, + &addr->scope_id, sizeof(uint8_t)); + } else { + return false; + } + } + if (n < 14 && dc == 42) return false; + if (n < 14) { + memmove(&addr->ip[dc + (14 - n)], &addr->ip[dc], n - dc + 2); + memset(&addr->ip[dc], 0, 14 - n); } - mg_byte_reverse(ctx->in, 14); - - a = (uint32_t *) ctx->in; - a[14] = ctx->bits[0]; - a[15] = ctx->bits[1]; - mg_md5_transform(ctx->buf, (uint32_t *) ctx->in); - mg_byte_reverse((unsigned char *) ctx->buf, 4); - memcpy(digest, ctx->buf, 16); - memset((char *) ctx, 0, sizeof(*ctx)); + addr->is_ip6 = true; + return true; } -#endif -#ifdef MG_ENABLE_LINES -#line 1 "src/mqtt.c" -#endif +bool mg_aton(struct mg_str str, struct mg_addr *addr) { + // MG_INFO(("[%.*s]", (int) str.len, str.buf)); + return mg_atone(str, addr) || mg_atonl(str, addr) || mg_aton4(str, addr) || + mg_aton6(str, addr); +} +struct mg_connection *mg_alloc_conn(struct mg_mgr *mgr) { + struct mg_connection *c = + (struct mg_connection *) calloc(1, sizeof(*c) + mgr->extraconnsize); + if (c != NULL) { + c->mgr = mgr; + c->send.align = c->recv.align = c->rtls.align = MG_IO_SIZE; + c->id = ++mgr->nextid; + MG_PROF_INIT(c); + } + return c; +} +void mg_close_conn(struct mg_connection *c) { + mg_resolve_cancel(c); // Close any pending DNS query + LIST_DELETE(struct mg_connection, &c->mgr->conns, c); + if (c == c->mgr->dns4.c) c->mgr->dns4.c = NULL; + if (c == c->mgr->dns6.c) c->mgr->dns6.c = NULL; + // Order of operations is important. `MG_EV_CLOSE` event must be fired + // before we deallocate received data, see #1331 + mg_call(c, MG_EV_CLOSE, NULL); + MG_DEBUG(("%lu %ld closed", c->id, c->fd)); + MG_PROF_DUMP(c); + MG_PROF_FREE(c); + mg_tls_free(c); + mg_iobuf_free(&c->recv); + mg_iobuf_free(&c->send); + mg_iobuf_free(&c->rtls); + mg_bzero((unsigned char *) c, sizeof(*c)); + free(c); +} +struct mg_connection *mg_connect(struct mg_mgr *mgr, const char *url, + mg_event_handler_t fn, void *fn_data) { + struct mg_connection *c = NULL; + if (url == NULL || url[0] == '\0') { + MG_ERROR(("null url")); + } else if ((c = mg_alloc_conn(mgr)) == NULL) { + MG_ERROR(("OOM")); + } else { + LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c); + c->is_udp = (strncmp(url, "udp:", 4) == 0); + c->fd = (void *) (size_t) MG_INVALID_SOCKET; + c->fn = fn; + c->is_client = true; + c->fn_data = fn_data; + MG_DEBUG(("%lu %ld %s", c->id, c->fd, url)); + mg_call(c, MG_EV_OPEN, (void *) url); + mg_resolve(c, url); + } + return c; +} +struct mg_connection *mg_listen(struct mg_mgr *mgr, const char *url, + mg_event_handler_t fn, void *fn_data) { + struct mg_connection *c = NULL; + if ((c = mg_alloc_conn(mgr)) == NULL) { + MG_ERROR(("OOM %s", url)); + } else if (!mg_open_listener(c, url)) { + MG_ERROR(("Failed: %s, errno %d", url, errno)); + MG_PROF_FREE(c); + free(c); + c = NULL; + } else { + c->is_listening = 1; + c->is_udp = strncmp(url, "udp:", 4) == 0; + LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c); + c->fn = fn; + c->fn_data = fn_data; + mg_call(c, MG_EV_OPEN, NULL); + if (mg_url_is_ssl(url)) c->is_tls = 1; // Accepted connection must + MG_DEBUG(("%lu %ld %s", c->id, c->fd, url)); + } + return c; +} +struct mg_connection *mg_wrapfd(struct mg_mgr *mgr, int fd, + mg_event_handler_t fn, void *fn_data) { + struct mg_connection *c = mg_alloc_conn(mgr); + if (c != NULL) { + c->fd = (void *) (size_t) fd; + c->fn = fn; + c->fn_data = fn_data; + MG_EPOLL_ADD(c); + mg_call(c, MG_EV_OPEN, NULL); + LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c); + } + return c; +} +struct mg_timer *mg_timer_add(struct mg_mgr *mgr, uint64_t milliseconds, + unsigned flags, void (*fn)(void *), void *arg) { + struct mg_timer *t = (struct mg_timer *) calloc(1, sizeof(*t)); + if (t != NULL) { + mg_timer_init(&mgr->timers, t, milliseconds, flags, fn, arg); + t->id = mgr->timerid++; + } + return t; +} -#define MQTT_CLEAN_SESSION 0x02 -#define MQTT_HAS_WILL 0x04 -#define MQTT_WILL_RETAIN 0x20 -#define MQTT_HAS_PASSWORD 0x40 -#define MQTT_HAS_USER_NAME 0x80 +long mg_io_recv(struct mg_connection *c, void *buf, size_t len) { + if (c->rtls.len == 0) return MG_IO_WAIT; + if (len > c->rtls.len) len = c->rtls.len; + memcpy(buf, c->rtls.buf, len); + mg_iobuf_del(&c->rtls, 0, len); + return (long) len; +} -struct mg_mqtt_pmap { - uint8_t id; - uint8_t type; -}; +void mg_mgr_free(struct mg_mgr *mgr) { + struct mg_connection *c; + struct mg_timer *tmp, *t = mgr->timers; + while (t != NULL) tmp = t->next, free(t), t = tmp; + mgr->timers = NULL; // Important. Next call to poll won't touch timers + for (c = mgr->conns; c != NULL; c = c->next) c->is_closing = 1; + mg_mgr_poll(mgr, 0); +#if MG_ENABLE_FREERTOS_TCP + FreeRTOS_DeleteSocketSet(mgr->ss); +#endif + MG_DEBUG(("All connections closed")); +#if MG_ENABLE_EPOLL + if (mgr->epoll_fd >= 0) close(mgr->epoll_fd), mgr->epoll_fd = -1; +#endif + mg_tls_ctx_free(mgr); +} -static const struct mg_mqtt_pmap s_prop_map[] = { - {MQTT_PROP_PAYLOAD_FORMAT_INDICATOR, MQTT_PROP_TYPE_BYTE}, - {MQTT_PROP_MESSAGE_EXPIRY_INTERVAL, MQTT_PROP_TYPE_INT}, - {MQTT_PROP_CONTENT_TYPE, MQTT_PROP_TYPE_STRING}, - {MQTT_PROP_RESPONSE_TOPIC, MQTT_PROP_TYPE_STRING}, - {MQTT_PROP_CORRELATION_DATA, MQTT_PROP_TYPE_BINARY_DATA}, - {MQTT_PROP_SUBSCRIPTION_IDENTIFIER, MQTT_PROP_TYPE_VARIABLE_INT}, - {MQTT_PROP_SESSION_EXPIRY_INTERVAL, MQTT_PROP_TYPE_INT}, - {MQTT_PROP_ASSIGNED_CLIENT_IDENTIFIER, MQTT_PROP_TYPE_STRING}, - {MQTT_PROP_SERVER_KEEP_ALIVE, MQTT_PROP_TYPE_SHORT}, - {MQTT_PROP_AUTHENTICATION_METHOD, MQTT_PROP_TYPE_STRING}, - {MQTT_PROP_AUTHENTICATION_DATA, MQTT_PROP_TYPE_BINARY_DATA}, - {MQTT_PROP_REQUEST_PROBLEM_INFORMATION, MQTT_PROP_TYPE_BYTE}, - {MQTT_PROP_WILL_DELAY_INTERVAL, MQTT_PROP_TYPE_INT}, - {MQTT_PROP_REQUEST_RESPONSE_INFORMATION, MQTT_PROP_TYPE_BYTE}, - {MQTT_PROP_RESPONSE_INFORMATION, MQTT_PROP_TYPE_STRING}, - {MQTT_PROP_SERVER_REFERENCE, MQTT_PROP_TYPE_STRING}, - {MQTT_PROP_REASON_STRING, MQTT_PROP_TYPE_STRING}, - {MQTT_PROP_RECEIVE_MAXIMUM, MQTT_PROP_TYPE_SHORT}, - {MQTT_PROP_TOPIC_ALIAS_MAXIMUM, MQTT_PROP_TYPE_SHORT}, - {MQTT_PROP_TOPIC_ALIAS, MQTT_PROP_TYPE_SHORT}, - {MQTT_PROP_MAXIMUM_QOS, MQTT_PROP_TYPE_BYTE}, - {MQTT_PROP_RETAIN_AVAILABLE, MQTT_PROP_TYPE_BYTE}, - {MQTT_PROP_USER_PROPERTY, MQTT_PROP_TYPE_STRING_PAIR}, - {MQTT_PROP_MAXIMUM_PACKET_SIZE, MQTT_PROP_TYPE_INT}, - {MQTT_PROP_WILDCARD_SUBSCRIPTION_AVAILABLE, MQTT_PROP_TYPE_BYTE}, - {MQTT_PROP_SUBSCRIPTION_IDENTIFIER_AVAILABLE, MQTT_PROP_TYPE_BYTE}, - {MQTT_PROP_SHARED_SUBSCRIPTION_AVAILABLE, MQTT_PROP_TYPE_BYTE}}; - -void mg_mqtt_send_header(struct mg_connection *c, uint8_t cmd, uint8_t flags, - uint32_t len) { - uint8_t buf[1 + sizeof(len)], *vlen = &buf[1]; - buf[0] = (uint8_t) ((cmd << 4) | flags); - do { - *vlen = len % 0x80; - len /= 0x80; - if (len > 0) *vlen |= 0x80; - vlen++; - } while (len > 0 && vlen < &buf[sizeof(buf)]); - mg_send(c, buf, (size_t) (vlen - buf)); +void mg_mgr_init(struct mg_mgr *mgr) { + memset(mgr, 0, sizeof(*mgr)); +#if MG_ENABLE_EPOLL + if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) + MG_ERROR(("epoll_create1 errno %d", errno)); +#else + mgr->epoll_fd = -1; +#endif +#if MG_ARCH == MG_ARCH_WIN32 && MG_ENABLE_WINSOCK + // clang-format off + { WSADATA data; WSAStartup(MAKEWORD(2, 2), &data); } + // clang-format on +#elif MG_ENABLE_FREERTOS_TCP + mgr->ss = FreeRTOS_CreateSocketSet(); +#elif defined(__unix) || defined(__unix__) || defined(__APPLE__) + // Ignore SIGPIPE signal, so if client cancels the request, it + // won't kill the whole process. + signal(SIGPIPE, SIG_IGN); +#elif MG_ENABLE_TCPIP_DRIVER_INIT && defined(MG_TCPIP_DRIVER_INIT) + MG_TCPIP_DRIVER_INIT(mgr); +#endif + mgr->pipe = MG_INVALID_SOCKET; + mgr->dnstimeout = 3000; + mgr->dns4.url = "udp://8.8.8.8:53"; + mgr->dns6.url = "udp://[2001:4860:4860::8888]:53"; + mg_tls_ctx_init(mgr); } -static void mg_send_u16(struct mg_connection *c, uint16_t value) { - mg_send(c, &value, sizeof(value)); -} +#ifdef MG_ENABLE_LINES +#line 1 "src/net_builtin.c" +#endif -static void mg_send_u32(struct mg_connection *c, uint32_t value) { - mg_send(c, &value, sizeof(value)); -} -static uint8_t varint_size(size_t length) { - uint8_t bytes_needed = 0; - do { - bytes_needed++; - length /= 0x80; - } while (length > 0); - return bytes_needed; -} +#if defined(MG_ENABLE_TCPIP) && MG_ENABLE_TCPIP +#define MG_EPHEMERAL_PORT_BASE 32768 +#define PDIFF(a, b) ((size_t) (((char *) (b)) - ((char *) (a)))) -static size_t encode_varint(uint8_t *buf, size_t value) { - size_t len = 0; +#ifndef MIP_TCP_KEEPALIVE_MS +#define MIP_TCP_KEEPALIVE_MS 45000 // TCP keep-alive period, ms +#endif - do { - uint8_t b = (uint8_t) (value % 128); - value /= 128; - if (value > 0) b |= 0x80; - buf[len++] = b; - } while (value > 0); +#define MIP_TCP_ACK_MS 150 // Timeout for ACKing +#define MIP_ARP_RESP_MS 100 // Timeout for ARP response +#define MIP_TCP_SYN_MS 15000 // Timeout for connection establishment +#define MIP_TCP_FIN_MS 1000 // Timeout for closing connection +#define MIP_TCP_WIN 6000 // TCP window size - return len; -} +struct connstate { + uint32_t seq, ack; // TCP seq/ack counters + uint64_t timer; // TCP keep-alive / ACK timer + uint32_t acked; // Last ACK-ed number + size_t unacked; // Not acked bytes + uint8_t mac[6]; // Peer MAC address + uint8_t ttype; // Timer type. 0: ack, 1: keep-alive +#define MIP_TTYPE_KEEPALIVE 0 // Connection is idle for long, send keepalive +#define MIP_TTYPE_ACK 1 // Peer sent us data, we have to ack it soon +#define MIP_TTYPE_ARP 2 // ARP resolve sent, waiting for response +#define MIP_TTYPE_SYN 3 // SYN sent, waiting for response +#define MIP_TTYPE_FIN 4 // FIN sent, waiting until terminating the connection + uint8_t tmiss; // Number of keep-alive misses + struct mg_iobuf raw; // For TLS only. Incoming raw data +}; -static size_t decode_varint(const uint8_t *buf, size_t len, size_t *value) { - size_t multiplier = 1, offset; - *value = 0; +#pragma pack(push, 1) - for (offset = 0; offset < 4 && offset < len; offset++) { - uint8_t encoded_byte = buf[offset]; - *value += (encoded_byte & 0x7f) * multiplier; - multiplier *= 128; +struct lcp { + uint8_t addr, ctrl, proto[2], code, id, len[2]; +}; - if ((encoded_byte & 0x80) == 0) return offset + 1; - } +struct eth { + uint8_t dst[6]; // Destination MAC address + uint8_t src[6]; // Source MAC address + uint16_t type; // Ethernet type +}; - return 0; -} +struct ip { + uint8_t ver; // Version + uint8_t tos; // Unused + uint16_t len; // Length + uint16_t id; // Unused + uint16_t frag; // Fragmentation +#define IP_FRAG_OFFSET_MSK 0x1fff +#define IP_MORE_FRAGS_MSK 0x2000 + uint8_t ttl; // Time to live + uint8_t proto; // Upper level protocol + uint16_t csum; // Checksum + uint32_t src; // Source IP + uint32_t dst; // Destination IP +}; -static int mqtt_prop_type_by_id(uint8_t prop_id) { - size_t i, num_properties = sizeof(s_prop_map) / sizeof(s_prop_map[0]); - for (i = 0; i < num_properties; ++i) { - if (s_prop_map[i].id == prop_id) return s_prop_map[i].type; - } - return -1; // Property ID not found -} +struct ip6 { + uint8_t ver; // Version + uint8_t opts[3]; // Options + uint16_t len; // Length + uint8_t proto; // Upper level protocol + uint8_t ttl; // Time to live + uint8_t src[16]; // Source IP + uint8_t dst[16]; // Destination IP +}; -// Returns the size of the properties section, without the -// size of the content's length -static size_t get_properties_length(struct mg_mqtt_prop *props, size_t count) { - size_t i, size = 0; - for (i = 0; i < count; i++) { - size++; // identifier - switch (mqtt_prop_type_by_id(props[i].id)) { - case MQTT_PROP_TYPE_STRING_PAIR: - size += (uint32_t) (props[i].val.len + props[i].key.len + - 2 * sizeof(uint16_t)); - break; - case MQTT_PROP_TYPE_STRING: - size += (uint32_t) (props[i].val.len + sizeof(uint16_t)); - break; - case MQTT_PROP_TYPE_BINARY_DATA: - size += (uint32_t) (props[i].val.len + sizeof(uint16_t)); - break; - case MQTT_PROP_TYPE_VARIABLE_INT: - size += varint_size((uint32_t) props[i].iv); - break; - case MQTT_PROP_TYPE_INT: - size += (uint32_t) sizeof(uint32_t); - break; - case MQTT_PROP_TYPE_SHORT: - size += (uint32_t) sizeof(uint16_t); - break; - case MQTT_PROP_TYPE_BYTE: - size += (uint32_t) sizeof(uint8_t); - break; - default: - return size; // cannot parse further down - } - } +struct icmp { + uint8_t type; + uint8_t code; + uint16_t csum; +}; - return size; -} +struct arp { + uint16_t fmt; // Format of hardware address + uint16_t pro; // Format of protocol address + uint8_t hlen; // Length of hardware address + uint8_t plen; // Length of protocol address + uint16_t op; // Operation + uint8_t sha[6]; // Sender hardware address + uint32_t spa; // Sender protocol address + uint8_t tha[6]; // Target hardware address + uint32_t tpa; // Target protocol address +}; -// returns the entire size of the properties section, including the -// size of the variable length of the content -static size_t get_props_size(struct mg_mqtt_prop *props, size_t count) { - size_t size = get_properties_length(props, count); - size += varint_size(size); - return size; -} +struct tcp { + uint16_t sport; // Source port + uint16_t dport; // Destination port + uint32_t seq; // Sequence number + uint32_t ack; // Acknowledgement number + uint8_t off; // Data offset + uint8_t flags; // TCP flags +#define TH_FIN 0x01 +#define TH_SYN 0x02 +#define TH_RST 0x04 +#define TH_PUSH 0x08 +#define TH_ACK 0x10 +#define TH_URG 0x20 +#define TH_ECE 0x40 +#define TH_CWR 0x80 + uint16_t win; // Window + uint16_t csum; // Checksum + uint16_t urp; // Urgent pointer +}; -static void mg_send_mqtt_properties(struct mg_connection *c, - struct mg_mqtt_prop *props, size_t nprops) { - size_t total_size = get_properties_length(props, nprops); - uint8_t buf_v[4] = {0, 0, 0, 0}; - uint8_t buf[4] = {0, 0, 0, 0}; - size_t i, len = encode_varint(buf, total_size); +struct udp { + uint16_t sport; // Source port + uint16_t dport; // Destination port + uint16_t len; // UDP length + uint16_t csum; // UDP checksum +}; - mg_send(c, buf, (size_t) len); - for (i = 0; i < nprops; i++) { - mg_send(c, &props[i].id, sizeof(props[i].id)); - switch (mqtt_prop_type_by_id(props[i].id)) { - case MQTT_PROP_TYPE_STRING_PAIR: - mg_send_u16(c, mg_htons((uint16_t) props[i].key.len)); - mg_send(c, props[i].key.buf, props[i].key.len); - mg_send_u16(c, mg_htons((uint16_t) props[i].val.len)); - mg_send(c, props[i].val.buf, props[i].val.len); - break; - case MQTT_PROP_TYPE_BYTE: - mg_send(c, &props[i].iv, sizeof(uint8_t)); - break; - case MQTT_PROP_TYPE_SHORT: - mg_send_u16(c, mg_htons((uint16_t) props[i].iv)); - break; - case MQTT_PROP_TYPE_INT: - mg_send_u32(c, mg_htonl((uint32_t) props[i].iv)); - break; - case MQTT_PROP_TYPE_STRING: - mg_send_u16(c, mg_htons((uint16_t) props[i].val.len)); - mg_send(c, props[i].val.buf, props[i].val.len); - break; - case MQTT_PROP_TYPE_BINARY_DATA: - mg_send_u16(c, mg_htons((uint16_t) props[i].val.len)); - mg_send(c, props[i].val.buf, props[i].val.len); - break; - case MQTT_PROP_TYPE_VARIABLE_INT: - len = encode_varint(buf_v, props[i].iv); - mg_send(c, buf_v, (size_t) len); - break; - } - } -} - -size_t mg_mqtt_next_prop(struct mg_mqtt_message *msg, struct mg_mqtt_prop *prop, - size_t ofs) { - uint8_t *i = (uint8_t *) msg->dgram.buf + msg->props_start + ofs; - uint8_t *end = (uint8_t *) msg->dgram.buf + msg->dgram.len; - size_t new_pos = ofs, len; - prop->id = i[0]; +struct dhcp { + uint8_t op, htype, hlen, hops; + uint32_t xid; + uint16_t secs, flags; + uint32_t ciaddr, yiaddr, siaddr, giaddr; + uint8_t hwaddr[208]; + uint32_t magic; + uint8_t options[32]; +}; - if (ofs >= msg->dgram.len || ofs >= msg->props_start + msg->props_size) - return 0; - i++, new_pos++; +#pragma pack(pop) - switch (mqtt_prop_type_by_id(prop->id)) { - case MQTT_PROP_TYPE_STRING_PAIR: - prop->key.len = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); - prop->key.buf = (char *) i + 2; - i += 2 + prop->key.len; - prop->val.len = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); - prop->val.buf = (char *) i + 2; - new_pos += 2 * sizeof(uint16_t) + prop->val.len + prop->key.len; - break; - case MQTT_PROP_TYPE_BYTE: - prop->iv = (uint8_t) i[0]; - new_pos++; - break; - case MQTT_PROP_TYPE_SHORT: - prop->iv = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); - new_pos += sizeof(uint16_t); - break; - case MQTT_PROP_TYPE_INT: - prop->iv = ((uint32_t) i[0] << 24) | ((uint32_t) i[1] << 16) | - ((uint32_t) i[2] << 8) | i[3]; - new_pos += sizeof(uint32_t); - break; - case MQTT_PROP_TYPE_STRING: - prop->val.len = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); - prop->val.buf = (char *) i + 2; - new_pos += 2 + prop->val.len; - break; - case MQTT_PROP_TYPE_BINARY_DATA: - prop->val.len = (uint16_t) ((((uint16_t) i[0]) << 8) | i[1]); - prop->val.buf = (char *) i + 2; - new_pos += 2 + prop->val.len; - break; - case MQTT_PROP_TYPE_VARIABLE_INT: - len = decode_varint(i, (size_t) (end - i), (size_t *) &prop->iv); - new_pos = (!len) ? 0 : new_pos + len; - break; - default: - new_pos = 0; - } +struct pkt { + struct mg_str raw; // Raw packet data + struct mg_str pay; // Payload data + struct eth *eth; + struct llc *llc; + struct arp *arp; + struct ip *ip; + struct ip6 *ip6; + struct icmp *icmp; + struct tcp *tcp; + struct udp *udp; + struct dhcp *dhcp; +}; - return new_pos; +static void mg_tcpip_call(struct mg_tcpip_if *ifp, int ev, void *ev_data) { + if (ifp->fn != NULL) ifp->fn(ifp, ev, ev_data); } -void mg_mqtt_login(struct mg_connection *c, const struct mg_mqtt_opts *opts) { - char client_id[21]; - struct mg_str cid = opts->client_id; - size_t total_len = 7 + 1 + 2 + 2; - uint8_t hdr[8] = {0, 4, 'M', 'Q', 'T', 'T', opts->version, 0}; - - if (cid.len == 0) { - mg_random_str(client_id, sizeof(client_id) - 1); - client_id[sizeof(client_id) - 1] = '\0'; - cid = mg_str(client_id); - } +static void send_syn(struct mg_connection *c); - if (hdr[6] == 0) hdr[6] = 4; // If version is not set, use 4 (3.1.1) - c->is_mqtt5 = hdr[6] == 5; // Set version 5 flag - hdr[7] = (uint8_t) ((opts->qos & 3) << 3); // Connection flags - if (opts->user.len > 0) { - total_len += 2 + (uint32_t) opts->user.len; - hdr[7] |= MQTT_HAS_USER_NAME; - } - if (opts->pass.len > 0) { - total_len += 2 + (uint32_t) opts->pass.len; - hdr[7] |= MQTT_HAS_PASSWORD; - } - if (opts->topic.len > 0) { // allow zero-length msgs, message.len is size_t - total_len += 4 + (uint32_t) opts->topic.len + (uint32_t) opts->message.len; - hdr[7] |= MQTT_HAS_WILL; - } - if (opts->clean || cid.len == 0) hdr[7] |= MQTT_CLEAN_SESSION; - if (opts->retain) hdr[7] |= MQTT_WILL_RETAIN; - total_len += (uint32_t) cid.len; - if (c->is_mqtt5) { - total_len += get_props_size(opts->props, opts->num_props); - if (hdr[7] & MQTT_HAS_WILL) - total_len += get_props_size(opts->will_props, opts->num_will_props); - } +static void mkpay(struct pkt *pkt, void *p) { + pkt->pay = + mg_str_n((char *) p, (size_t) (&pkt->raw.buf[pkt->raw.len] - (char *) p)); +} - mg_mqtt_send_header(c, MQTT_CMD_CONNECT, 0, (uint32_t) total_len); - mg_send(c, hdr, sizeof(hdr)); - // keepalive == 0 means "do not disconnect us!" - mg_send_u16(c, mg_htons((uint16_t) opts->keepalive)); +static uint32_t csumup(uint32_t sum, const void *buf, size_t len) { + size_t i; + const uint8_t *p = (const uint8_t *) buf; + for (i = 0; i < len; i++) sum += i & 1 ? p[i] : (uint32_t) (p[i] << 8); + return sum; +} - if (c->is_mqtt5) mg_send_mqtt_properties(c, opts->props, opts->num_props); +static uint16_t csumfin(uint32_t sum) { + while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); + return mg_htons(~sum & 0xffff); +} - mg_send_u16(c, mg_htons((uint16_t) cid.len)); - mg_send(c, cid.buf, cid.len); +static uint16_t ipcsum(const void *buf, size_t len) { + uint32_t sum = csumup(0, buf, len); + return csumfin(sum); +} - if (hdr[7] & MQTT_HAS_WILL) { - if (c->is_mqtt5) - mg_send_mqtt_properties(c, opts->will_props, opts->num_will_props); +static void settmout(struct mg_connection *c, uint8_t type) { + struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; + struct connstate *s = (struct connstate *) (c + 1); + unsigned n = type == MIP_TTYPE_ACK ? MIP_TCP_ACK_MS + : type == MIP_TTYPE_ARP ? MIP_ARP_RESP_MS + : type == MIP_TTYPE_SYN ? MIP_TCP_SYN_MS + : type == MIP_TTYPE_FIN ? MIP_TCP_FIN_MS + : MIP_TCP_KEEPALIVE_MS; + s->timer = ifp->now + n; + s->ttype = type; + MG_VERBOSE(("%lu %d -> %llx", c->id, type, s->timer)); +} - mg_send_u16(c, mg_htons((uint16_t) opts->topic.len)); - mg_send(c, opts->topic.buf, opts->topic.len); - mg_send_u16(c, mg_htons((uint16_t) opts->message.len)); - mg_send(c, opts->message.buf, opts->message.len); - } - if (opts->user.len > 0) { - mg_send_u16(c, mg_htons((uint16_t) opts->user.len)); - mg_send(c, opts->user.buf, opts->user.len); - } - if (opts->pass.len > 0) { - mg_send_u16(c, mg_htons((uint16_t) opts->pass.len)); - mg_send(c, opts->pass.buf, opts->pass.len); - } +static size_t ether_output(struct mg_tcpip_if *ifp, size_t len) { + size_t n = ifp->driver->tx(ifp->tx.buf, len, ifp); + if (n == len) ifp->nsent++; + return n; } -uint16_t mg_mqtt_pub(struct mg_connection *c, const struct mg_mqtt_opts *opts) { - uint16_t id = opts->retransmit_id; - uint8_t flags = (uint8_t) (((opts->qos & 3) << 1) | (opts->retain ? 1 : 0)); - size_t len = 2 + opts->topic.len + opts->message.len; - MG_DEBUG(("%lu [%.*s] -> [%.*s]", c->id, (int) opts->topic.len, - (char *) opts->topic.buf, (int) opts->message.len, - (char *) opts->message.buf)); - if (opts->qos > 0) len += 2; - if (c->is_mqtt5) len += get_props_size(opts->props, opts->num_props); +void mg_tcpip_arp_request(struct mg_tcpip_if *ifp, uint32_t ip, uint8_t *mac) { + struct eth *eth = (struct eth *) ifp->tx.buf; + struct arp *arp = (struct arp *) (eth + 1); + memset(eth->dst, 255, sizeof(eth->dst)); + memcpy(eth->src, ifp->mac, sizeof(eth->src)); + eth->type = mg_htons(0x806); + memset(arp, 0, sizeof(*arp)); + arp->fmt = mg_htons(1), arp->pro = mg_htons(0x800), arp->hlen = 6, + arp->plen = 4; + arp->op = mg_htons(1), arp->tpa = ip, arp->spa = ifp->ip; + memcpy(arp->sha, ifp->mac, sizeof(arp->sha)); + if (mac != NULL) memcpy(arp->tha, mac, sizeof(arp->tha)); + ether_output(ifp, PDIFF(eth, arp + 1)); +} - if (opts->qos > 0 && id != 0) flags |= 1 << 3; - mg_mqtt_send_header(c, MQTT_CMD_PUBLISH, flags, (uint32_t) len); - mg_send_u16(c, mg_htons((uint16_t) opts->topic.len)); - mg_send(c, opts->topic.buf, opts->topic.len); - if (opts->qos > 0) { // need to send 'id' field - if (id == 0) { // generate new one if not resending - if (++c->mgr->mqtt_id == 0) ++c->mgr->mqtt_id; - id = c->mgr->mqtt_id; - } - mg_send_u16(c, mg_htons(id)); +static void onstatechange(struct mg_tcpip_if *ifp) { + if (ifp->state == MG_TCPIP_STATE_READY) { + MG_INFO(("READY, IP: %M", mg_print_ip4, &ifp->ip)); + MG_INFO((" GW: %M", mg_print_ip4, &ifp->gw)); + MG_INFO((" MAC: %M", mg_print_mac, &ifp->mac)); + } else if (ifp->state == MG_TCPIP_STATE_IP) { + MG_ERROR(("Got IP")); + mg_tcpip_arp_request(ifp, ifp->gw, NULL); // unsolicited GW ARP request + } else if (ifp->state == MG_TCPIP_STATE_UP) { + MG_ERROR(("Link up")); + srand((unsigned int) mg_millis()); + } else if (ifp->state == MG_TCPIP_STATE_DOWN) { + MG_ERROR(("Link down")); } - - if (c->is_mqtt5) mg_send_mqtt_properties(c, opts->props, opts->num_props); - - if (opts->message.len > 0) mg_send(c, opts->message.buf, opts->message.len); - return id; + mg_tcpip_call(ifp, MG_TCPIP_EV_ST_CHG, &ifp->state); } -void mg_mqtt_sub(struct mg_connection *c, const struct mg_mqtt_opts *opts) { - uint8_t qos_ = opts->qos & 3; - size_t plen = c->is_mqtt5 ? get_props_size(opts->props, opts->num_props) : 0; - size_t len = 2 + opts->topic.len + 2 + 1 + plen; - - mg_mqtt_send_header(c, MQTT_CMD_SUBSCRIBE, 2, (uint32_t) len); - if (++c->mgr->mqtt_id == 0) ++c->mgr->mqtt_id; - mg_send_u16(c, mg_htons(c->mgr->mqtt_id)); - if (c->is_mqtt5) mg_send_mqtt_properties(c, opts->props, opts->num_props); - - mg_send_u16(c, mg_htons((uint16_t) opts->topic.len)); - mg_send(c, opts->topic.buf, opts->topic.len); - mg_send(c, &qos_, sizeof(qos_)); +static struct ip *tx_ip(struct mg_tcpip_if *ifp, uint8_t *mac_dst, + uint8_t proto, uint32_t ip_src, uint32_t ip_dst, + size_t plen) { + struct eth *eth = (struct eth *) ifp->tx.buf; + struct ip *ip = (struct ip *) (eth + 1); + memcpy(eth->dst, mac_dst, sizeof(eth->dst)); + memcpy(eth->src, ifp->mac, sizeof(eth->src)); // Use our MAC + eth->type = mg_htons(0x800); + memset(ip, 0, sizeof(*ip)); + ip->ver = 0x45; // Version 4, header length 5 words + ip->frag = mg_htons(0x4000); // Don't fragment + ip->len = mg_htons((uint16_t) (sizeof(*ip) + plen)); + ip->ttl = 64; + ip->proto = proto; + ip->src = ip_src; + ip->dst = ip_dst; + ip->csum = ipcsum(ip, sizeof(*ip)); + return ip; } -int mg_mqtt_parse(const uint8_t *buf, size_t len, uint8_t version, - struct mg_mqtt_message *m) { - uint8_t lc = 0, *p, *end; - uint32_t n = 0, len_len = 0; - - memset(m, 0, sizeof(*m)); - m->dgram.buf = (char *) buf; - if (len < 2) return MQTT_INCOMPLETE; - m->cmd = (uint8_t) (buf[0] >> 4); - m->qos = (buf[0] >> 1) & 3; - - n = len_len = 0; - p = (uint8_t *) buf + 1; - while ((size_t) (p - buf) < len) { - lc = *((uint8_t *) p++); - n += (uint32_t) ((lc & 0x7f) << 7 * len_len); - len_len++; - if (!(lc & 0x80)) break; - if (len_len >= 4) return MQTT_MALFORMED; - } - end = p + n; - if ((lc & 0x80) || (end > buf + len)) return MQTT_INCOMPLETE; - m->dgram.len = (size_t) (end - buf); - - switch (m->cmd) { - case MQTT_CMD_CONNACK: - if (end - p < 2) return MQTT_MALFORMED; - m->ack = p[1]; - break; - case MQTT_CMD_PUBACK: - case MQTT_CMD_PUBREC: - case MQTT_CMD_PUBREL: - case MQTT_CMD_PUBCOMP: - case MQTT_CMD_SUBSCRIBE: - case MQTT_CMD_SUBACK: - case MQTT_CMD_UNSUBSCRIBE: - case MQTT_CMD_UNSUBACK: - if (p + 2 > end) return MQTT_MALFORMED; - m->id = (uint16_t) ((((uint16_t) p[0]) << 8) | p[1]); - p += 2; - break; - case MQTT_CMD_PUBLISH: { - if (p + 2 > end) return MQTT_MALFORMED; - m->topic.len = (uint16_t) ((((uint16_t) p[0]) << 8) | p[1]); - m->topic.buf = (char *) p + 2; - p += 2 + m->topic.len; - if (p > end) return MQTT_MALFORMED; - if (m->qos > 0) { - if (p + 2 > end) return MQTT_MALFORMED; - m->id = (uint16_t) ((((uint16_t) p[0]) << 8) | p[1]); - p += 2; - } - if (p > end) return MQTT_MALFORMED; - if (version == 5 && p + 2 < end) { - len_len = - (uint32_t) decode_varint(p, (size_t) (end - p), &m->props_size); - if (!len_len) return MQTT_MALFORMED; - m->props_start = (size_t) (p + len_len - buf); - p += len_len + m->props_size; - } - if (p > end) return MQTT_MALFORMED; - m->data.buf = (char *) p; - m->data.len = (size_t) (end - p); - break; - } - default: - break; - } - return MQTT_OK; +static void tx_udp(struct mg_tcpip_if *ifp, uint8_t *mac_dst, uint32_t ip_src, + uint16_t sport, uint32_t ip_dst, uint16_t dport, + const void *buf, size_t len) { + struct ip *ip = + tx_ip(ifp, mac_dst, 17, ip_src, ip_dst, len + sizeof(struct udp)); + struct udp *udp = (struct udp *) (ip + 1); + // MG_DEBUG(("UDP XX LEN %d %d", (int) len, (int) ifp->tx.len)); + udp->sport = sport; + udp->dport = dport; + udp->len = mg_htons((uint16_t) (sizeof(*udp) + len)); + udp->csum = 0; + uint32_t cs = csumup(0, udp, sizeof(*udp)); + cs = csumup(cs, buf, len); + cs = csumup(cs, &ip->src, sizeof(ip->src)); + cs = csumup(cs, &ip->dst, sizeof(ip->dst)); + cs += (uint32_t) (ip->proto + sizeof(*udp) + len); + udp->csum = csumfin(cs); + memmove(udp + 1, buf, len); + // MG_DEBUG(("UDP LEN %d %d", (int) len, (int) ifp->frame_len)); + ether_output(ifp, sizeof(struct eth) + sizeof(*ip) + sizeof(*udp) + len); } -static void mqtt_cb(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_READ) { - for (;;) { - uint8_t version = c->is_mqtt5 ? 5 : 4; - struct mg_mqtt_message mm; - int rc = mg_mqtt_parse(c->recv.buf, c->recv.len, version, &mm); - if (rc == MQTT_MALFORMED) { - MG_ERROR(("%lu MQTT malformed message", c->id)); - c->is_closing = 1; - break; - } else if (rc == MQTT_OK) { - MG_VERBOSE(("%lu MQTT CMD %d len %d [%.*s]", c->id, mm.cmd, - (int) mm.dgram.len, (int) mm.data.len, mm.data.buf)); - switch (mm.cmd) { - case MQTT_CMD_CONNACK: - mg_call(c, MG_EV_MQTT_OPEN, &mm.ack); - if (mm.ack == 0) { - MG_DEBUG(("%lu Connected", c->id)); - } else { - MG_ERROR(("%lu MQTT auth failed, code %d", c->id, mm.ack)); - c->is_closing = 1; - } - break; - case MQTT_CMD_PUBLISH: { - /*MG_DEBUG(("%lu [%.*s] -> [%.*s]", c->id, (int) mm.topic.len, - mm.topic.buf, (int) mm.data.len, mm.data.buf));*/ - if (mm.qos > 0) { - uint16_t id = mg_ntohs(mm.id); - uint32_t remaining_len = sizeof(id); - if (c->is_mqtt5) remaining_len += 2; // 3.4.2 +static void tx_dhcp(struct mg_tcpip_if *ifp, uint8_t *mac_dst, uint32_t ip_src, + uint32_t ip_dst, uint8_t *opts, size_t optslen, + bool ciaddr) { + // https://datatracker.ietf.org/doc/html/rfc2132#section-9.6 + struct dhcp dhcp = {1, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, {0}, 0, {0}}; + dhcp.magic = mg_htonl(0x63825363); + memcpy(&dhcp.hwaddr, ifp->mac, sizeof(ifp->mac)); + memcpy(&dhcp.xid, ifp->mac + 2, sizeof(dhcp.xid)); + memcpy(&dhcp.options, opts, optslen); + if (ciaddr) dhcp.ciaddr = ip_src; + tx_udp(ifp, mac_dst, ip_src, mg_htons(68), ip_dst, mg_htons(67), &dhcp, + sizeof(dhcp)); +} - mg_mqtt_send_header( - c, - (uint8_t) (mm.qos == 2 ? MQTT_CMD_PUBREC : MQTT_CMD_PUBACK), - 0, remaining_len); - mg_send(c, &id, sizeof(id)); +static const uint8_t broadcast[] = {255, 255, 255, 255, 255, 255}; - if (c->is_mqtt5) { - uint16_t zero = 0; - mg_send(c, &zero, sizeof(zero)); - } - } - mg_call(c, MG_EV_MQTT_MSG, &mm); // let the app handle qos stuff - break; - } - case MQTT_CMD_PUBREC: { // MQTT5: 3.5.2-1 TODO(): variable header rc - uint16_t id = mg_ntohs(mm.id); - uint32_t remaining_len = sizeof(id); // MQTT5 3.6.2-1 - mg_mqtt_send_header(c, MQTT_CMD_PUBREL, 2, remaining_len); - mg_send(c, &id, sizeof(id)); // MQTT5 3.6.1-1, flags = 2 - break; - } - case MQTT_CMD_PUBREL: { // MQTT5: 3.6.2-1 TODO(): variable header rc - uint16_t id = mg_ntohs(mm.id); - uint32_t remaining_len = sizeof(id); // MQTT5 3.7.2-1 - mg_mqtt_send_header(c, MQTT_CMD_PUBCOMP, 0, remaining_len); - mg_send(c, &id, sizeof(id)); - break; - } - } - mg_call(c, MG_EV_MQTT_CMD, &mm); - mg_iobuf_del(&c->recv, 0, mm.dgram.len); - } else { - break; - } - } - } - (void) ev_data; +// RFC-2131 #4.3.6, #4.4.1; RFC-2132 #9.8 +static void tx_dhcp_request_sel(struct mg_tcpip_if *ifp, uint32_t ip_req, + uint32_t ip_srv) { + uint8_t opts[] = { + 53, 1, 3, // Type: DHCP request + 12, 3, 'm', 'i', 'p', // Host name: "mip" + 54, 4, 0, 0, 0, 0, // DHCP server ID + 50, 4, 0, 0, 0, 0, // Requested IP + 55, 2, 1, 3, 255, 255, // GW, mask [DNS] [SNTP] + 255 // End of options + }; + uint8_t addopts = 0; + memcpy(opts + 10, &ip_srv, sizeof(ip_srv)); + memcpy(opts + 16, &ip_req, sizeof(ip_req)); + if (ifp->enable_req_dns) opts[24 + addopts++] = 6; // DNS + if (ifp->enable_req_sntp) opts[24 + addopts++] = 42; // SNTP + opts[21] += addopts; + tx_dhcp(ifp, (uint8_t *) broadcast, 0, 0xffffffff, opts, + sizeof(opts) + addopts - 2, false); + MG_DEBUG(("DHCP req sent")); } -void mg_mqtt_ping(struct mg_connection *nc) { - mg_mqtt_send_header(nc, MQTT_CMD_PINGREQ, 0, 0); +// RFC-2131 #4.3.6, #4.4.5 (renewing: unicast, rebinding: bcast) +static void tx_dhcp_request_re(struct mg_tcpip_if *ifp, uint8_t *mac_dst, + uint32_t ip_src, uint32_t ip_dst) { + uint8_t opts[] = { + 53, 1, 3, // Type: DHCP request + 255 // End of options + }; + tx_dhcp(ifp, mac_dst, ip_src, ip_dst, opts, sizeof(opts), true); + MG_DEBUG(("DHCP req sent")); } -void mg_mqtt_pong(struct mg_connection *nc) { - mg_mqtt_send_header(nc, MQTT_CMD_PINGRESP, 0, 0); +static void tx_dhcp_discover(struct mg_tcpip_if *ifp) { + uint8_t opts[] = { + 53, 1, 1, // Type: DHCP discover + 55, 2, 1, 3, // Parameters: ip, mask + 255 // End of options + }; + tx_dhcp(ifp, (uint8_t *) broadcast, 0, 0xffffffff, opts, sizeof(opts), false); + MG_DEBUG(("DHCP discover sent. Our MAC: %M", mg_print_mac, ifp->mac)); } -void mg_mqtt_disconnect(struct mg_connection *c, - const struct mg_mqtt_opts *opts) { - size_t len = 0; - if (c->is_mqtt5) len = 1 + get_props_size(opts->props, opts->num_props); - mg_mqtt_send_header(c, MQTT_CMD_DISCONNECT, 0, (uint32_t) len); - - if (c->is_mqtt5) { - uint8_t zero = 0; - mg_send(c, &zero, sizeof(zero)); // reason code - mg_send_mqtt_properties(c, opts->props, opts->num_props); +static struct mg_connection *getpeer(struct mg_mgr *mgr, struct pkt *pkt, + bool lsn) { + struct mg_connection *c = NULL; + for (c = mgr->conns; c != NULL; c = c->next) { + if (c->is_arplooking && pkt->arp && + memcmp(&pkt->arp->spa, c->rem.ip, sizeof(pkt->arp->spa)) == 0) + break; + if (c->is_udp && pkt->udp && c->loc.port == pkt->udp->dport) break; + if (!c->is_udp && pkt->tcp && c->loc.port == pkt->tcp->dport && + lsn == c->is_listening && (lsn || c->rem.port == pkt->tcp->sport)) + break; } + return c; } -struct mg_connection *mg_mqtt_connect(struct mg_mgr *mgr, const char *url, - const struct mg_mqtt_opts *opts, - mg_event_handler_t fn, void *fn_data) { - struct mg_connection *c = mg_connect(mgr, url, fn, fn_data); - if (c != NULL) { - struct mg_mqtt_opts empty; - memset(&empty, 0, sizeof(empty)); - mg_mqtt_login(c, opts == NULL ? &empty : opts); - c->pfn = mqtt_cb; - } - return c; -} - -struct mg_connection *mg_mqtt_listen(struct mg_mgr *mgr, const char *url, - mg_event_handler_t fn, void *fn_data) { - struct mg_connection *c = mg_listen(mgr, url, fn, fn_data); - if (c != NULL) c->pfn = mqtt_cb, c->pfn_data = mgr; - return c; -} - -#ifdef MG_ENABLE_LINES -#line 1 "src/net.c" -#endif - - - - - - - - +static void mac_resolved(struct mg_connection *c); -size_t mg_vprintf(struct mg_connection *c, const char *fmt, va_list *ap) { - size_t old = c->send.len; - mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, ap); - return c->send.len - old; +static void rx_arp(struct mg_tcpip_if *ifp, struct pkt *pkt) { + if (pkt->arp->op == mg_htons(1) && pkt->arp->tpa == ifp->ip) { + // ARP request. Make a response, then send + // MG_DEBUG(("ARP op %d %M: %M", mg_ntohs(pkt->arp->op), mg_print_ip4, + // &pkt->arp->spa, mg_print_ip4, &pkt->arp->tpa)); + struct eth *eth = (struct eth *) ifp->tx.buf; + struct arp *arp = (struct arp *) (eth + 1); + memcpy(eth->dst, pkt->eth->src, sizeof(eth->dst)); + memcpy(eth->src, ifp->mac, sizeof(eth->src)); + eth->type = mg_htons(0x806); + *arp = *pkt->arp; + arp->op = mg_htons(2); + memcpy(arp->tha, pkt->arp->sha, sizeof(pkt->arp->tha)); + memcpy(arp->sha, ifp->mac, sizeof(pkt->arp->sha)); + arp->tpa = pkt->arp->spa; + arp->spa = ifp->ip; + MG_DEBUG(("ARP: tell %M we're %M", mg_print_ip4, &arp->tpa, mg_print_mac, + &ifp->mac)); + ether_output(ifp, PDIFF(eth, arp + 1)); + } else if (pkt->arp->op == mg_htons(2)) { + if (memcmp(pkt->arp->tha, ifp->mac, sizeof(pkt->arp->tha)) != 0) return; + if (pkt->arp->spa == ifp->gw) { + // Got response for the GW ARP request. Set ifp->gwmac and IP -> READY + memcpy(ifp->gwmac, pkt->arp->sha, sizeof(ifp->gwmac)); + if (ifp->state == MG_TCPIP_STATE_IP) { + ifp->state = MG_TCPIP_STATE_READY; + onstatechange(ifp); + } + } else { + struct mg_connection *c = getpeer(ifp->mgr, pkt, false); + if (c != NULL && c->is_arplooking) { + struct connstate *s = (struct connstate *) (c + 1); + memcpy(s->mac, pkt->arp->sha, sizeof(s->mac)); + MG_DEBUG(("%lu ARP resolved %M -> %M", c->id, mg_print_ip4, c->rem.ip, + mg_print_mac, s->mac)); + c->is_arplooking = 0; + mac_resolved(c); + } + } + } } -size_t mg_printf(struct mg_connection *c, const char *fmt, ...) { - size_t len = 0; - va_list ap; - va_start(ap, fmt); - len = mg_vprintf(c, fmt, &ap); - va_end(ap); - return len; +static void rx_icmp(struct mg_tcpip_if *ifp, struct pkt *pkt) { + // MG_DEBUG(("ICMP %d", (int) len)); + if (pkt->icmp->type == 8 && pkt->ip != NULL && pkt->ip->dst == ifp->ip) { + size_t hlen = sizeof(struct eth) + sizeof(struct ip) + sizeof(struct icmp); + size_t space = ifp->tx.len - hlen, plen = pkt->pay.len; + if (plen > space) plen = space; + struct ip *ip = tx_ip(ifp, pkt->eth->src, 1, ifp->ip, pkt->ip->src, + sizeof(struct icmp) + plen); + struct icmp *icmp = (struct icmp *) (ip + 1); + memset(icmp, 0, sizeof(*icmp)); // Set csum to 0 + memcpy(icmp + 1, pkt->pay.buf, plen); // Copy RX payload to TX + icmp->csum = ipcsum(icmp, sizeof(*icmp) + plen); + ether_output(ifp, hlen + plen); + } } -static bool mg_atonl(struct mg_str str, struct mg_addr *addr) { - uint32_t localhost = mg_htonl(0x7f000001); - if (mg_strcasecmp(str, mg_str("localhost")) != 0) return false; - memcpy(addr->ip, &localhost, sizeof(uint32_t)); - addr->is_ip6 = false; - return true; +static void rx_dhcp_client(struct mg_tcpip_if *ifp, struct pkt *pkt) { + uint32_t ip = 0, gw = 0, mask = 0, lease = 0, dns = 0, sntp = 0; + uint8_t msgtype = 0, state = ifp->state; + // perform size check first, then access fields + uint8_t *p = pkt->dhcp->options, + *end = (uint8_t *) &pkt->raw.buf[pkt->raw.len]; + if (end < (uint8_t *) (pkt->dhcp + 1)) return; + if (memcmp(&pkt->dhcp->xid, ifp->mac + 2, sizeof(pkt->dhcp->xid))) return; + while (p + 1 < end && p[0] != 255) { // Parse options RFC-1533 #9 + if (p[0] == 1 && p[1] == sizeof(ifp->mask) && p + 6 < end) { // Mask + memcpy(&mask, p + 2, sizeof(mask)); + } else if (p[0] == 3 && p[1] == sizeof(ifp->gw) && p + 6 < end) { // GW + memcpy(&gw, p + 2, sizeof(gw)); + ip = pkt->dhcp->yiaddr; + } else if (ifp->enable_req_dns && p[0] == 6 && p[1] == sizeof(dns) && + p + 6 < end) { // DNS + memcpy(&dns, p + 2, sizeof(dns)); + } else if (ifp->enable_req_sntp && p[0] == 42 && p[1] == sizeof(sntp) && + p + 6 < end) { // SNTP + memcpy(&sntp, p + 2, sizeof(sntp)); + } else if (p[0] == 51 && p[1] == 4 && p + 6 < end) { // Lease + memcpy(&lease, p + 2, sizeof(lease)); + lease = mg_ntohl(lease); + } else if (p[0] == 53 && p[1] == 1 && p + 6 < end) { // Msg Type + msgtype = p[2]; + } + p += p[1] + 2; + } + // Process message type, RFC-1533 (9.4); RFC-2131 (3.1, 4) + if (msgtype == 6 && ifp->ip == ip) { // DHCPNACK, release IP + ifp->state = MG_TCPIP_STATE_UP, ifp->ip = 0; + } else if (msgtype == 2 && ifp->state == MG_TCPIP_STATE_UP && ip && gw && + lease) { // DHCPOFFER + // select IP, (4.4.1) (fallback to IP source addr on foul play) + tx_dhcp_request_sel(ifp, ip, + pkt->dhcp->siaddr ? pkt->dhcp->siaddr : pkt->ip->src); + ifp->state = MG_TCPIP_STATE_REQ; // REQUESTING state + } else if (msgtype == 5) { // DHCPACK + if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) { // got an IP + ifp->lease_expire = ifp->now + lease * 1000; + MG_INFO(("Lease: %u sec (%lld)", lease, ifp->lease_expire / 1000)); + // assume DHCP server = router until ARP resolves + memcpy(ifp->gwmac, pkt->eth->src, sizeof(ifp->gwmac)); + ifp->ip = ip, ifp->gw = gw, ifp->mask = mask; + ifp->state = MG_TCPIP_STATE_IP; // BOUND state + uint64_t rand; + mg_random(&rand, sizeof(rand)); + srand((unsigned int) (rand + mg_millis())); + if (ifp->enable_req_dns && dns != 0) + mg_tcpip_call(ifp, MG_TCPIP_EV_DHCP_DNS, &dns); + if (ifp->enable_req_sntp && sntp != 0) + mg_tcpip_call(ifp, MG_TCPIP_EV_DHCP_SNTP, &sntp); + } else if (ifp->state == MG_TCPIP_STATE_READY && ifp->ip == ip) { // renew + ifp->lease_expire = ifp->now + lease * 1000; + MG_INFO(("Lease: %u sec (%lld)", lease, ifp->lease_expire / 1000)); + } // TODO(): accept provided T1/T2 and store server IP for renewal (4.4) + } + if (ifp->state != state) onstatechange(ifp); } -static bool mg_atone(struct mg_str str, struct mg_addr *addr) { - if (str.len > 0) return false; - memset(addr->ip, 0, sizeof(addr->ip)); - addr->is_ip6 = false; - return true; +// Simple DHCP server that assigns a next IP address: ifp->ip + 1 +static void rx_dhcp_server(struct mg_tcpip_if *ifp, struct pkt *pkt) { + uint8_t op = 0, *p = pkt->dhcp->options, + *end = (uint8_t *) &pkt->raw.buf[pkt->raw.len]; + if (end < (uint8_t *) (pkt->dhcp + 1)) return; + // struct dhcp *req = pkt->dhcp; + struct dhcp res = {2, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, {0}, 0, {0}}; + res.yiaddr = ifp->ip; + ((uint8_t *) (&res.yiaddr))[3]++; // Offer our IP + 1 + while (p + 1 < end && p[0] != 255) { // Parse options + if (p[0] == 53 && p[1] == 1 && p + 2 < end) { // Message type + op = p[2]; + } + p += p[1] + 2; + } + if (op == 1 || op == 3) { // DHCP Discover or DHCP Request + uint8_t msg = op == 1 ? 2 : 5; // Message type: DHCP OFFER or DHCP ACK + uint8_t opts[] = { + 53, 1, msg, // Message type + 1, 4, 0, 0, 0, 0, // Subnet mask + 54, 4, 0, 0, 0, 0, // Server ID + 12, 3, 'm', 'i', 'p', // Host name: "mip" + 51, 4, 255, 255, 255, 255, // Lease time + 255 // End of options + }; + memcpy(&res.hwaddr, pkt->dhcp->hwaddr, 6); + memcpy(opts + 5, &ifp->mask, sizeof(ifp->mask)); + memcpy(opts + 11, &ifp->ip, sizeof(ifp->ip)); + memcpy(&res.options, opts, sizeof(opts)); + res.magic = pkt->dhcp->magic; + res.xid = pkt->dhcp->xid; + if (ifp->enable_get_gateway) { + ifp->gw = res.yiaddr; // set gw IP, best-effort gwmac as DHCP server's + memcpy(ifp->gwmac, pkt->eth->src, sizeof(ifp->gwmac)); + } + tx_udp(ifp, pkt->eth->src, ifp->ip, mg_htons(67), + op == 1 ? ~0U : res.yiaddr, mg_htons(68), &res, sizeof(res)); + } } -static bool mg_aton4(struct mg_str str, struct mg_addr *addr) { - uint8_t data[4] = {0, 0, 0, 0}; - size_t i, num_dots = 0; - for (i = 0; i < str.len; i++) { - if (str.buf[i] >= '0' && str.buf[i] <= '9') { - int octet = data[num_dots] * 10 + (str.buf[i] - '0'); - if (octet > 255) return false; - data[num_dots] = (uint8_t) octet; - } else if (str.buf[i] == '.') { - if (num_dots >= 3 || i == 0 || str.buf[i - 1] == '.') return false; - num_dots++; +static void rx_udp(struct mg_tcpip_if *ifp, struct pkt *pkt) { + struct mg_connection *c = getpeer(ifp->mgr, pkt, true); + if (c == NULL) { + // No UDP listener on this port. Should send ICMP, but keep silent. + } else { + c->rem.port = pkt->udp->sport; + memcpy(c->rem.ip, &pkt->ip->src, sizeof(uint32_t)); + struct connstate *s = (struct connstate *) (c + 1); + memcpy(s->mac, pkt->eth->src, sizeof(s->mac)); + if (c->recv.len >= MG_MAX_RECV_SIZE) { + mg_error(c, "max_recv_buf_size reached"); + } else if (c->recv.size - c->recv.len < pkt->pay.len && + !mg_iobuf_resize(&c->recv, c->recv.len + pkt->pay.len)) { + mg_error(c, "oom"); } else { - return false; + memcpy(&c->recv.buf[c->recv.len], pkt->pay.buf, pkt->pay.len); + c->recv.len += pkt->pay.len; + mg_call(c, MG_EV_READ, &pkt->pay.len); } } - if (num_dots != 3 || str.buf[i - 1] == '.') return false; - memcpy(&addr->ip, data, sizeof(data)); - addr->is_ip6 = false; - return true; } -static bool mg_v4mapped(struct mg_str str, struct mg_addr *addr) { - int i; - uint32_t ipv4; - if (str.len < 14) return false; - if (str.buf[0] != ':' || str.buf[1] != ':' || str.buf[6] != ':') return false; - for (i = 2; i < 6; i++) { - if (str.buf[i] != 'f' && str.buf[i] != 'F') return false; - } - // struct mg_str s = mg_str_n(&str.buf[7], str.len - 7); - if (!mg_aton4(mg_str_n(&str.buf[7], str.len - 7), addr)) return false; - memcpy(&ipv4, addr->ip, sizeof(ipv4)); - memset(addr->ip, 0, sizeof(addr->ip)); - addr->ip[10] = addr->ip[11] = 255; - memcpy(&addr->ip[12], &ipv4, 4); - addr->is_ip6 = true; - return true; -} - -static bool mg_aton6(struct mg_str str, struct mg_addr *addr) { - size_t i, j = 0, n = 0, dc = 42; - addr->scope_id = 0; - if (str.len > 2 && str.buf[0] == '[') str.buf++, str.len -= 2; - if (mg_v4mapped(str, addr)) return true; - for (i = 0; i < str.len; i++) { - if ((str.buf[i] >= '0' && str.buf[i] <= '9') || - (str.buf[i] >= 'a' && str.buf[i] <= 'f') || - (str.buf[i] >= 'A' && str.buf[i] <= 'F')) { - unsigned long val = 0; // TODO(): This loops on chars, refactor - if (i > j + 3) return false; - // MG_DEBUG(("%lu %lu [%.*s]", i, j, (int) (i - j + 1), &str.buf[j])); - mg_str_to_num(mg_str_n(&str.buf[j], i - j + 1), 16, &val, sizeof(val)); - addr->ip[n] = (uint8_t) ((val >> 8) & 255); - addr->ip[n + 1] = (uint8_t) (val & 255); - } else if (str.buf[i] == ':') { - j = i + 1; - if (i > 0 && str.buf[i - 1] == ':') { - dc = n; // Double colon - if (i > 1 && str.buf[i - 2] == ':') return false; - } else if (i > 0) { - n += 2; - } - if (n > 14) return false; - addr->ip[n] = addr->ip[n + 1] = 0; // For trailing :: - } else if (str.buf[i] == '%') { // Scope ID, last in string - return mg_str_to_num(mg_str_n(&str.buf[i + 1], str.len - i - 1), 10, - &addr->scope_id, sizeof(uint8_t)); - } else { - return false; - } - } - if (n < 14 && dc == 42) return false; - if (n < 14) { - memmove(&addr->ip[dc + (14 - n)], &addr->ip[dc], n - dc + 2); - memset(&addr->ip[dc], 0, 14 - n); +static size_t tx_tcp(struct mg_tcpip_if *ifp, uint8_t *dst_mac, uint32_t dst_ip, + uint8_t flags, uint16_t sport, uint16_t dport, + uint32_t seq, uint32_t ack, const void *buf, size_t len) { +#if 0 + uint8_t opts[] = {2, 4, 5, 0xb4, 4, 2, 0, 0}; // MSS = 1460, SACK permitted + if (flags & TH_SYN) { + // Handshake? Set MSS + buf = opts; + len = sizeof(opts); } +#endif + struct ip *ip = + tx_ip(ifp, dst_mac, 6, ifp->ip, dst_ip, sizeof(struct tcp) + len); + struct tcp *tcp = (struct tcp *) (ip + 1); + memset(tcp, 0, sizeof(*tcp)); + if (buf != NULL && len) memmove(tcp + 1, buf, len); + tcp->sport = sport; + tcp->dport = dport; + tcp->seq = seq; + tcp->ack = ack; + tcp->flags = flags; + tcp->win = mg_htons(MIP_TCP_WIN); + tcp->off = (uint8_t) (sizeof(*tcp) / 4 << 4); + // if (flags & TH_SYN) tcp->off = 0x70; // Handshake? header size 28 bytes - addr->is_ip6 = true; - return true; + uint32_t cs = 0; + uint16_t n = (uint16_t) (sizeof(*tcp) + len); + uint8_t pseudo[] = {0, ip->proto, (uint8_t) (n >> 8), (uint8_t) (n & 255)}; + cs = csumup(cs, tcp, n); + cs = csumup(cs, &ip->src, sizeof(ip->src)); + cs = csumup(cs, &ip->dst, sizeof(ip->dst)); + cs = csumup(cs, pseudo, sizeof(pseudo)); + tcp->csum = csumfin(cs); + MG_VERBOSE(("TCP %M:%hu -> %M:%hu fl %x len %u", mg_print_ip4, &ip->src, + mg_ntohs(tcp->sport), mg_print_ip4, &ip->dst, + mg_ntohs(tcp->dport), tcp->flags, len)); + // mg_hexdump(ifp->tx.buf, PDIFF(ifp->tx.buf, tcp + 1) + len); + return ether_output(ifp, PDIFF(ifp->tx.buf, tcp + 1) + len); } -bool mg_aton(struct mg_str str, struct mg_addr *addr) { - // MG_INFO(("[%.*s]", (int) str.len, str.buf)); - return mg_atone(str, addr) || mg_atonl(str, addr) || mg_aton4(str, addr) || - mg_aton6(str, addr); +static size_t tx_tcp_pkt(struct mg_tcpip_if *ifp, struct pkt *pkt, + uint8_t flags, uint32_t seq, const void *buf, + size_t len) { + uint32_t delta = (pkt->tcp->flags & (TH_SYN | TH_FIN)) ? 1 : 0; + return tx_tcp(ifp, pkt->eth->src, pkt->ip->src, flags, pkt->tcp->dport, + pkt->tcp->sport, seq, mg_htonl(mg_ntohl(pkt->tcp->seq) + delta), + buf, len); } -struct mg_connection *mg_alloc_conn(struct mg_mgr *mgr) { - struct mg_connection *c = - (struct mg_connection *) calloc(1, sizeof(*c) + mgr->extraconnsize); - if (c != NULL) { - c->mgr = mgr; - c->send.align = c->recv.align = c->rtls.align = MG_IO_SIZE; - c->id = ++mgr->nextid; - MG_PROF_INIT(c); +static struct mg_connection *accept_conn(struct mg_connection *lsn, + struct pkt *pkt) { + struct mg_connection *c = mg_alloc_conn(lsn->mgr); + if (c == NULL) { + MG_ERROR(("OOM")); + return NULL; } + struct connstate *s = (struct connstate *) (c + 1); + s->seq = mg_ntohl(pkt->tcp->ack), s->ack = mg_ntohl(pkt->tcp->seq); + memcpy(s->mac, pkt->eth->src, sizeof(s->mac)); + settmout(c, MIP_TTYPE_KEEPALIVE); + memcpy(c->rem.ip, &pkt->ip->src, sizeof(uint32_t)); + c->rem.port = pkt->tcp->sport; + MG_DEBUG(("%lu accepted %M", c->id, mg_print_ip_port, &c->rem)); + LIST_ADD_HEAD(struct mg_connection, &lsn->mgr->conns, c); + c->is_accepted = 1; + c->is_hexdumping = lsn->is_hexdumping; + c->pfn = lsn->pfn; + c->loc = lsn->loc; + c->pfn_data = lsn->pfn_data; + c->fn = lsn->fn; + c->fn_data = lsn->fn_data; + mg_call(c, MG_EV_OPEN, NULL); + mg_call(c, MG_EV_ACCEPT, NULL); return c; } -void mg_close_conn(struct mg_connection *c) { - mg_resolve_cancel(c); // Close any pending DNS query - LIST_DELETE(struct mg_connection, &c->mgr->conns, c); - if (c == c->mgr->dns4.c) c->mgr->dns4.c = NULL; - if (c == c->mgr->dns6.c) c->mgr->dns6.c = NULL; - // Order of operations is important. `MG_EV_CLOSE` event must be fired - // before we deallocate received data, see #1331 - mg_call(c, MG_EV_CLOSE, NULL); - MG_DEBUG(("%lu %ld closed", c->id, c->fd)); - MG_PROF_DUMP(c); - MG_PROF_FREE(c); - - mg_tls_free(c); - mg_iobuf_free(&c->recv); - mg_iobuf_free(&c->send); - mg_iobuf_free(&c->rtls); - mg_bzero((unsigned char *) c, sizeof(*c)); - free(c); -} +static size_t trim_len(struct mg_connection *c, size_t len) { + struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; + size_t eth_h_len = 14, ip_max_h_len = 24, tcp_max_h_len = 60, udp_h_len = 8; + size_t max_headers_len = + eth_h_len + ip_max_h_len + (c->is_udp ? udp_h_len : tcp_max_h_len); + size_t min_mtu = c->is_udp ? 68 /* RFC-791 */ : max_headers_len - eth_h_len; -struct mg_connection *mg_connect(struct mg_mgr *mgr, const char *url, - mg_event_handler_t fn, void *fn_data) { - struct mg_connection *c = NULL; - if (url == NULL || url[0] == '\0') { - MG_ERROR(("null url")); - } else if ((c = mg_alloc_conn(mgr)) == NULL) { - MG_ERROR(("OOM")); - } else { - LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c); - c->is_udp = (strncmp(url, "udp:", 4) == 0); - c->fd = (void *) (size_t) MG_INVALID_SOCKET; - c->fn = fn; - c->is_client = true; - c->fn_data = fn_data; - MG_DEBUG(("%lu %ld %s", c->id, c->fd, url)); - mg_call(c, MG_EV_OPEN, (void *) url); - mg_resolve(c, url); + // If the frame exceeds the available buffer, trim the length + if (len + max_headers_len > ifp->tx.len) { + len = ifp->tx.len - max_headers_len; } - return c; -} - -struct mg_connection *mg_listen(struct mg_mgr *mgr, const char *url, - mg_event_handler_t fn, void *fn_data) { - struct mg_connection *c = NULL; - if ((c = mg_alloc_conn(mgr)) == NULL) { - MG_ERROR(("OOM %s", url)); - } else if (!mg_open_listener(c, url)) { - MG_ERROR(("Failed: %s, errno %d", url, errno)); - MG_PROF_FREE(c); - free(c); - c = NULL; - } else { - c->is_listening = 1; - c->is_udp = strncmp(url, "udp:", 4) == 0; - LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c); - c->fn = fn; - c->fn_data = fn_data; - mg_call(c, MG_EV_OPEN, NULL); - if (mg_url_is_ssl(url)) c->is_tls = 1; // Accepted connection must - MG_DEBUG(("%lu %ld %s", c->id, c->fd, url)); + // Ensure the MTU isn't lower than the minimum allowed value + if (ifp->mtu < min_mtu) { + MG_ERROR(("MTU is lower than minimum, capping to %lu", min_mtu)); + ifp->mtu = (uint16_t) min_mtu; } - return c; -} - -struct mg_connection *mg_wrapfd(struct mg_mgr *mgr, int fd, - mg_event_handler_t fn, void *fn_data) { - struct mg_connection *c = mg_alloc_conn(mgr); - if (c != NULL) { - c->fd = (void *) (size_t) fd; - c->fn = fn; - c->fn_data = fn_data; - MG_EPOLL_ADD(c); - mg_call(c, MG_EV_OPEN, NULL); - LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c); + // If the total packet size exceeds the MTU, trim the length + if (len + max_headers_len - eth_h_len > ifp->mtu) { + len = ifp->mtu - max_headers_len + eth_h_len; + if (c->is_udp) { + MG_ERROR(("UDP datagram exceeds MTU. Truncating it.")); + } } - return c; + + return len; } -struct mg_timer *mg_timer_add(struct mg_mgr *mgr, uint64_t milliseconds, - unsigned flags, void (*fn)(void *), void *arg) { - struct mg_timer *t = (struct mg_timer *) calloc(1, sizeof(*t)); - if (t != NULL) { - mg_timer_init(&mgr->timers, t, milliseconds, flags, fn, arg); - t->id = mgr->timerid++; +long mg_io_send(struct mg_connection *c, const void *buf, size_t len) { + struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; + struct connstate *s = (struct connstate *) (c + 1); + uint32_t dst_ip = *(uint32_t *) c->rem.ip; + len = trim_len(c, len); + if (c->is_udp) { + tx_udp(ifp, s->mac, ifp->ip, c->loc.port, dst_ip, c->rem.port, buf, len); + } else { + size_t sent = + tx_tcp(ifp, s->mac, dst_ip, TH_PUSH | TH_ACK, c->loc.port, c->rem.port, + mg_htonl(s->seq), mg_htonl(s->ack), buf, len); + if (sent == 0) { + return MG_IO_WAIT; + } else if (sent == (size_t) -1) { + return MG_IO_ERR; + } else { + s->seq += (uint32_t) len; + if (s->ttype == MIP_TTYPE_ACK) settmout(c, MIP_TTYPE_KEEPALIVE); + } } - return t; + return (long) len; } -long mg_io_recv(struct mg_connection *c, void *buf, size_t len) { - if (c->rtls.len == 0) return MG_IO_WAIT; - if (len > c->rtls.len) len = c->rtls.len; - memcpy(buf, c->rtls.buf, len); - mg_iobuf_del(&c->rtls, 0, len); - return (long) len; +static void handle_tls_recv(struct mg_connection *c, struct mg_iobuf *io) { + long n = mg_tls_recv(c, &io->buf[io->len], io->size - io->len); + if (n == MG_IO_ERR) { + mg_error(c, "TLS recv error"); + } else if (n > 0) { + // Decrypted successfully - trigger MG_EV_READ + io->len += (size_t) n; + mg_call(c, MG_EV_READ, &n); + } } -void mg_mgr_free(struct mg_mgr *mgr) { - struct mg_connection *c; - struct mg_timer *tmp, *t = mgr->timers; - while (t != NULL) tmp = t->next, free(t), t = tmp; - mgr->timers = NULL; // Important. Next call to poll won't touch timers - for (c = mgr->conns; c != NULL; c = c->next) c->is_closing = 1; - mg_mgr_poll(mgr, 0); -#if MG_ENABLE_FREERTOS_TCP - FreeRTOS_DeleteSocketSet(mgr->ss); -#endif - MG_DEBUG(("All connections closed")); -#if MG_ENABLE_EPOLL - if (mgr->epoll_fd >= 0) close(mgr->epoll_fd), mgr->epoll_fd = -1; -#endif - mg_tls_ctx_free(mgr); -} +static void read_conn(struct mg_connection *c, struct pkt *pkt) { + struct connstate *s = (struct connstate *) (c + 1); + struct mg_iobuf *io = c->is_tls ? &c->rtls : &c->recv; + uint32_t seq = mg_ntohl(pkt->tcp->seq); + uint32_t rem_ip; + memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); + if (pkt->tcp->flags & TH_FIN) { + // If we initiated the closure, we reply with ACK upon receiving FIN + // If we didn't initiate it, we reply with FIN as part of the normal TCP + // closure process + uint8_t flags = TH_ACK; + s->ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len + 1); + if (c->is_draining && s->ttype == MIP_TTYPE_FIN) { + if (s->seq == mg_htonl(pkt->tcp->ack)) { // Simultaneous closure ? + s->seq++; // Yes. Increment our SEQ + } else { // Otherwise, + s->seq = mg_htonl(pkt->tcp->ack); // Set to peer's ACK + } + } else { + flags |= TH_FIN; + c->is_draining = 1; + settmout(c, MIP_TTYPE_FIN); + } + tx_tcp((struct mg_tcpip_if *) c->mgr->priv, s->mac, rem_ip, flags, + c->loc.port, c->rem.port, mg_htonl(s->seq), mg_htonl(s->ack), "", 0); + } else if (pkt->pay.len == 0) { + // TODO(cpq): handle this peer's ACK + } else if (seq != s->ack) { + uint32_t ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len); + if (s->ack == ack) { + MG_VERBOSE(("ignoring duplicate pkt")); + } else { + MG_VERBOSE(("SEQ != ACK: %x %x %x", seq, s->ack, ack)); + tx_tcp((struct mg_tcpip_if *) c->mgr->priv, s->mac, rem_ip, TH_ACK, + c->loc.port, c->rem.port, mg_htonl(s->seq), mg_htonl(s->ack), "", + 0); + } + } else if (io->size - io->len < pkt->pay.len && + !mg_iobuf_resize(io, io->len + pkt->pay.len)) { + mg_error(c, "oom"); + } else { + // Copy TCP payload into the IO buffer. If the connection is plain text, + // we copy to c->recv. If the connection is TLS, this data is encrypted, + // therefore we copy that encrypted data to the c->rtls iobuffer instead, + // and then call mg_tls_recv() to decrypt it. NOTE: mg_tls_recv() will + // call back mg_io_recv() which grabs raw data from c->rtls + memcpy(&io->buf[io->len], pkt->pay.buf, pkt->pay.len); + io->len += pkt->pay.len; -void mg_mgr_init(struct mg_mgr *mgr) { - memset(mgr, 0, sizeof(*mgr)); -#if MG_ENABLE_EPOLL - if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) - MG_ERROR(("epoll_create1 errno %d", errno)); -#else - mgr->epoll_fd = -1; -#endif -#if MG_ARCH == MG_ARCH_WIN32 && MG_ENABLE_WINSOCK - // clang-format off - { WSADATA data; WSAStartup(MAKEWORD(2, 2), &data); } - // clang-format on -#elif MG_ENABLE_FREERTOS_TCP - mgr->ss = FreeRTOS_CreateSocketSet(); -#elif defined(__unix) || defined(__unix__) || defined(__APPLE__) - // Ignore SIGPIPE signal, so if client cancels the request, it - // won't kill the whole process. - signal(SIGPIPE, SIG_IGN); -#elif MG_ENABLE_TCPIP_DRIVER_INIT && defined(MG_TCPIP_DRIVER_INIT) - MG_TCPIP_DRIVER_INIT(mgr); -#endif - mgr->pipe = MG_INVALID_SOCKET; - mgr->dnstimeout = 3000; - mgr->dns4.url = "udp://8.8.8.8:53"; - mgr->dns6.url = "udp://[2001:4860:4860::8888]:53"; - mg_tls_ctx_init(mgr); + MG_VERBOSE(("%lu SEQ %x -> %x", c->id, mg_htonl(pkt->tcp->seq), s->ack)); + // Advance ACK counter + s->ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len); + s->unacked += pkt->pay.len; + // size_t diff = s->acked <= s->ack ? s->ack - s->acked : s->ack; + if (s->unacked > MIP_TCP_WIN / 2 && s->acked != s->ack) { + // Send ACK immediately + MG_VERBOSE(("%lu imm ACK %lu", c->id, s->acked)); + tx_tcp((struct mg_tcpip_if *) c->mgr->priv, s->mac, rem_ip, TH_ACK, + c->loc.port, c->rem.port, mg_htonl(s->seq), mg_htonl(s->ack), NULL, + 0); + s->unacked = 0; + s->acked = s->ack; + if (s->ttype != MIP_TTYPE_KEEPALIVE) settmout(c, MIP_TTYPE_KEEPALIVE); + } else { + // if not already running, setup a timer to send an ACK later + if (s->ttype != MIP_TTYPE_ACK) settmout(c, MIP_TTYPE_ACK); + } + + if (c->is_tls && c->is_tls_hs) { + mg_tls_handshake(c); + } else if (c->is_tls) { + // TLS connection. Make room for decrypted data in c->recv + io = &c->recv; + if (io->size - io->len < pkt->pay.len && + !mg_iobuf_resize(io, io->len + pkt->pay.len)) { + mg_error(c, "oom"); + } else { + // Decrypt data directly into c->recv + handle_tls_recv(c, io); + } + } else { + // Plain text connection, data is already in c->recv, trigger + // MG_EV_READ + mg_call(c, MG_EV_READ, &pkt->pay.len); + } + } } -#ifdef MG_ENABLE_LINES -#line 1 "src/net_builtin.c" +static void rx_tcp(struct mg_tcpip_if *ifp, struct pkt *pkt) { + struct mg_connection *c = getpeer(ifp->mgr, pkt, false); + struct connstate *s = c == NULL ? NULL : (struct connstate *) (c + 1); +#if 0 + MG_INFO(("%lu %hhu %d", c ? c->id : 0, pkt->tcp->flags, (int) pkt->pay.len)); #endif - - -#if defined(MG_ENABLE_TCPIP) && MG_ENABLE_TCPIP -#define MG_EPHEMERAL_PORT_BASE 32768 -#define PDIFF(a, b) ((size_t) (((char *) (b)) - ((char *) (a)))) - -#ifndef MIP_TCP_KEEPALIVE_MS -#define MIP_TCP_KEEPALIVE_MS 45000 // TCP keep-alive period, ms + if (c != NULL && c->is_connecting && pkt->tcp->flags == (TH_SYN | TH_ACK)) { + s->seq = mg_ntohl(pkt->tcp->ack), s->ack = mg_ntohl(pkt->tcp->seq) + 1; + tx_tcp_pkt(ifp, pkt, TH_ACK, pkt->tcp->ack, NULL, 0); + c->is_connecting = 0; // Client connected + settmout(c, MIP_TTYPE_KEEPALIVE); + mg_call(c, MG_EV_CONNECT, NULL); // Let user know + if (c->is_tls_hs) mg_tls_handshake(c); + } else if (c != NULL && c->is_connecting && pkt->tcp->flags != TH_ACK) { + // mg_hexdump(pkt->raw.buf, pkt->raw.len); + tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0); + } else if (c != NULL && pkt->tcp->flags & TH_RST) { + mg_error(c, "peer RST"); // RFC-1122 4.2.2.13 + } else if (c != NULL) { +#if 0 + MG_DEBUG(("%lu %d %M:%hu -> %M:%hu", c->id, (int) pkt->raw.len, + mg_print_ip4, &pkt->ip->src, mg_ntohs(pkt->tcp->sport), + mg_print_ip4, &pkt->ip->dst, mg_ntohs(pkt->tcp->dport))); + mg_hexdump(pkt->pay.buf, pkt->pay.len); #endif + s->tmiss = 0; // Reset missed keep-alive counter + if (s->ttype == MIP_TTYPE_KEEPALIVE) // Advance keep-alive timer + settmout(c, + MIP_TTYPE_KEEPALIVE); // unless a former ACK timeout is pending + read_conn(c, pkt); // Override timer with ACK timeout if needed + } else if ((c = getpeer(ifp->mgr, pkt, true)) == NULL) { + tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0); + } else if (pkt->tcp->flags & TH_RST) { + if (c->is_accepted) mg_error(c, "peer RST"); // RFC-1122 4.2.2.13 + // ignore RST if not connected + } else if (pkt->tcp->flags & TH_SYN) { + // Use peer's source port as ISN, in order to recognise the handshake + uint32_t isn = mg_htonl((uint32_t) mg_ntohs(pkt->tcp->sport)); + tx_tcp_pkt(ifp, pkt, TH_SYN | TH_ACK, isn, NULL, 0); + } else if (pkt->tcp->flags & TH_FIN) { + tx_tcp_pkt(ifp, pkt, TH_FIN | TH_ACK, pkt->tcp->ack, NULL, 0); + } else if (mg_htonl(pkt->tcp->ack) == mg_htons(pkt->tcp->sport) + 1U) { + accept_conn(c, pkt); + } else if (!c->is_accepted) { // no peer + tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0); + } else { + // MG_VERBOSE(("dropped silently..")); + } +} -#define MIP_TCP_ACK_MS 150 // Timeout for ACKing -#define MIP_ARP_RESP_MS 100 // Timeout for ARP response -#define MIP_TCP_SYN_MS 15000 // Timeout for connection establishment -#define MIP_TCP_FIN_MS 1000 // Timeout for closing connection -#define MIP_TCP_WIN 6000 // TCP window size - -struct connstate { - uint32_t seq, ack; // TCP seq/ack counters - uint64_t timer; // TCP keep-alive / ACK timer - uint32_t acked; // Last ACK-ed number - size_t unacked; // Not acked bytes - uint8_t mac[6]; // Peer MAC address - uint8_t ttype; // Timer type. 0: ack, 1: keep-alive -#define MIP_TTYPE_KEEPALIVE 0 // Connection is idle for long, send keepalive -#define MIP_TTYPE_ACK 1 // Peer sent us data, we have to ack it soon -#define MIP_TTYPE_ARP 2 // ARP resolve sent, waiting for response -#define MIP_TTYPE_SYN 3 // SYN sent, waiting for response -#define MIP_TTYPE_FIN 4 // FIN sent, waiting until terminating the connection - uint8_t tmiss; // Number of keep-alive misses - struct mg_iobuf raw; // For TLS only. Incoming raw data -}; - -#pragma pack(push, 1) - -struct lcp { - uint8_t addr, ctrl, proto[2], code, id, len[2]; -}; - -struct eth { - uint8_t dst[6]; // Destination MAC address - uint8_t src[6]; // Source MAC address - uint16_t type; // Ethernet type -}; - -struct ip { - uint8_t ver; // Version - uint8_t tos; // Unused - uint16_t len; // Length - uint16_t id; // Unused - uint16_t frag; // Fragmentation -#define IP_FRAG_OFFSET_MSK 0x1fff -#define IP_MORE_FRAGS_MSK 0x2000 - uint8_t ttl; // Time to live - uint8_t proto; // Upper level protocol - uint16_t csum; // Checksum - uint32_t src; // Source IP - uint32_t dst; // Destination IP -}; - -struct ip6 { - uint8_t ver; // Version - uint8_t opts[3]; // Options - uint16_t len; // Length - uint8_t proto; // Upper level protocol - uint8_t ttl; // Time to live - uint8_t src[16]; // Source IP - uint8_t dst[16]; // Destination IP -}; - -struct icmp { - uint8_t type; - uint8_t code; - uint16_t csum; -}; - -struct arp { - uint16_t fmt; // Format of hardware address - uint16_t pro; // Format of protocol address - uint8_t hlen; // Length of hardware address - uint8_t plen; // Length of protocol address - uint16_t op; // Operation - uint8_t sha[6]; // Sender hardware address - uint32_t spa; // Sender protocol address - uint8_t tha[6]; // Target hardware address - uint32_t tpa; // Target protocol address -}; - -struct tcp { - uint16_t sport; // Source port - uint16_t dport; // Destination port - uint32_t seq; // Sequence number - uint32_t ack; // Acknowledgement number - uint8_t off; // Data offset - uint8_t flags; // TCP flags -#define TH_FIN 0x01 -#define TH_SYN 0x02 -#define TH_RST 0x04 -#define TH_PUSH 0x08 -#define TH_ACK 0x10 -#define TH_URG 0x20 -#define TH_ECE 0x40 -#define TH_CWR 0x80 - uint16_t win; // Window - uint16_t csum; // Checksum - uint16_t urp; // Urgent pointer -}; +static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) { + uint16_t frag = mg_ntohs(pkt->ip->frag); + if (frag & IP_MORE_FRAGS_MSK || frag & IP_FRAG_OFFSET_MSK) { + if (pkt->ip->proto == 17) pkt->udp = (struct udp *) (pkt->ip + 1); + if (pkt->ip->proto == 6) pkt->tcp = (struct tcp *) (pkt->ip + 1); + struct mg_connection *c = getpeer(ifp->mgr, pkt, false); + if (c) mg_error(c, "Received fragmented packet"); + } else if (pkt->ip->proto == 1) { + pkt->icmp = (struct icmp *) (pkt->ip + 1); + if (pkt->pay.len < sizeof(*pkt->icmp)) return; + mkpay(pkt, pkt->icmp + 1); + rx_icmp(ifp, pkt); + } else if (pkt->ip->proto == 17) { + pkt->udp = (struct udp *) (pkt->ip + 1); + if (pkt->pay.len < sizeof(*pkt->udp)) return; + mkpay(pkt, pkt->udp + 1); + MG_VERBOSE(("UDP %M:%hu -> %M:%hu len %u", mg_print_ip4, &pkt->ip->src, + mg_ntohs(pkt->udp->sport), mg_print_ip4, &pkt->ip->dst, + mg_ntohs(pkt->udp->dport), (int) pkt->pay.len)); + if (ifp->enable_dhcp_client && pkt->udp->dport == mg_htons(68)) { + pkt->dhcp = (struct dhcp *) (pkt->udp + 1); + mkpay(pkt, pkt->dhcp + 1); + rx_dhcp_client(ifp, pkt); + } else if (ifp->enable_dhcp_server && pkt->udp->dport == mg_htons(67)) { + pkt->dhcp = (struct dhcp *) (pkt->udp + 1); + mkpay(pkt, pkt->dhcp + 1); + rx_dhcp_server(ifp, pkt); + } else { + rx_udp(ifp, pkt); + } + } else if (pkt->ip->proto == 6) { + pkt->tcp = (struct tcp *) (pkt->ip + 1); + if (pkt->pay.len < sizeof(*pkt->tcp)) return; + mkpay(pkt, pkt->tcp + 1); + uint16_t iplen = mg_ntohs(pkt->ip->len); + uint16_t off = (uint16_t) (sizeof(*pkt->ip) + ((pkt->tcp->off >> 4) * 4U)); + if (iplen >= off) pkt->pay.len = (size_t) (iplen - off); + MG_VERBOSE(("TCP %M:%hu -> %M:%hu len %u", mg_print_ip4, &pkt->ip->src, + mg_ntohs(pkt->tcp->sport), mg_print_ip4, &pkt->ip->dst, + mg_ntohs(pkt->tcp->dport), (int) pkt->pay.len)); + rx_tcp(ifp, pkt); + } +} -struct udp { - uint16_t sport; // Source port - uint16_t dport; // Destination port - uint16_t len; // UDP length - uint16_t csum; // UDP checksum -}; +static void rx_ip6(struct mg_tcpip_if *ifp, struct pkt *pkt) { + // MG_DEBUG(("IP %d", (int) len)); + if (pkt->ip6->proto == 1 || pkt->ip6->proto == 58) { + pkt->icmp = (struct icmp *) (pkt->ip6 + 1); + if (pkt->pay.len < sizeof(*pkt->icmp)) return; + mkpay(pkt, pkt->icmp + 1); + rx_icmp(ifp, pkt); + } else if (pkt->ip6->proto == 17) { + pkt->udp = (struct udp *) (pkt->ip6 + 1); + if (pkt->pay.len < sizeof(*pkt->udp)) return; + // MG_DEBUG((" UDP %u %u -> %u", len, mg_htons(udp->sport), + // mg_htons(udp->dport))); + mkpay(pkt, pkt->udp + 1); + } +} -struct dhcp { - uint8_t op, htype, hlen, hops; - uint32_t xid; - uint16_t secs, flags; - uint32_t ciaddr, yiaddr, siaddr, giaddr; - uint8_t hwaddr[208]; - uint32_t magic; - uint8_t options[32]; -}; +static void mg_tcpip_rx(struct mg_tcpip_if *ifp, void *buf, size_t len) { + struct pkt pkt; + memset(&pkt, 0, sizeof(pkt)); + pkt.raw.buf = (char *) buf; + pkt.raw.len = len; + pkt.eth = (struct eth *) buf; + // mg_hexdump(buf, len > 16 ? 16: len); + if (pkt.raw.len < sizeof(*pkt.eth)) return; // Truncated - runt? + if (ifp->enable_mac_check && + memcmp(pkt.eth->dst, ifp->mac, sizeof(pkt.eth->dst)) != 0 && + memcmp(pkt.eth->dst, broadcast, sizeof(pkt.eth->dst)) != 0) + return; + if (ifp->enable_crc32_check && len > 4) { + len -= 4; // TODO(scaprile): check on bigendian + uint32_t crc = mg_crc32(0, (const char *) buf, len); + if (memcmp((void *) ((size_t) buf + len), &crc, sizeof(crc))) return; + } + if (pkt.eth->type == mg_htons(0x806)) { + pkt.arp = (struct arp *) (pkt.eth + 1); + if (sizeof(*pkt.eth) + sizeof(*pkt.arp) > pkt.raw.len) return; // Truncated + mg_tcpip_call(ifp, MG_TCPIP_EV_ARP, &pkt.raw); + rx_arp(ifp, &pkt); + } else if (pkt.eth->type == mg_htons(0x86dd)) { + pkt.ip6 = (struct ip6 *) (pkt.eth + 1); + if (pkt.raw.len < sizeof(*pkt.eth) + sizeof(*pkt.ip6)) return; // Truncated + if ((pkt.ip6->ver >> 4) != 0x6) return; // Not IP + mkpay(&pkt, pkt.ip6 + 1); + rx_ip6(ifp, &pkt); + } else if (pkt.eth->type == mg_htons(0x800)) { + pkt.ip = (struct ip *) (pkt.eth + 1); + if (pkt.raw.len < sizeof(*pkt.eth) + sizeof(*pkt.ip)) return; // Truncated + // Truncate frame to what IP header tells us + if ((size_t) mg_ntohs(pkt.ip->len) + sizeof(struct eth) < pkt.raw.len) { + pkt.raw.len = (size_t) mg_ntohs(pkt.ip->len) + sizeof(struct eth); + } + if (pkt.raw.len < sizeof(*pkt.eth) + sizeof(*pkt.ip)) return; // Truncated + if ((pkt.ip->ver >> 4) != 4) return; // Not IP + mkpay(&pkt, pkt.ip + 1); + rx_ip(ifp, &pkt); + } else { + MG_DEBUG(("Unknown eth type %x", mg_htons(pkt.eth->type))); + if (mg_log_level >= MG_LL_VERBOSE) mg_hexdump(buf, len >= 32 ? 32 : len); + } +} -#pragma pack(pop) +static void mg_tcpip_poll(struct mg_tcpip_if *ifp, uint64_t now) { + struct mg_connection *c; + bool expired_1000ms = mg_timer_expired(&ifp->timer_1000ms, 1000, now); + ifp->now = now; -struct pkt { - struct mg_str raw; // Raw packet data - struct mg_str pay; // Payload data - struct eth *eth; - struct llc *llc; - struct arp *arp; - struct ip *ip; - struct ip6 *ip6; - struct icmp *icmp; - struct tcp *tcp; - struct udp *udp; - struct dhcp *dhcp; -}; +#if MG_ENABLE_TCPIP_PRINT_DEBUG_STATS + if (expired_1000ms) { + const char *names[] = {"down", "up", "req", "ip", "ready"}; + MG_INFO(("Status: %s, IP: %M, rx:%u, tx:%u, dr:%u, er:%u", + names[ifp->state], mg_print_ip4, &ifp->ip, ifp->nrecv, ifp->nsent, + ifp->ndrop, ifp->nerr)); + } +#endif + // Handle gw ARP request timeout, order is important + if (expired_1000ms && ifp->state == MG_TCPIP_STATE_IP) { + ifp->state = MG_TCPIP_STATE_READY; // keep best-effort MAC + onstatechange(ifp); + } + // Handle physical interface up/down status + if (expired_1000ms && ifp->driver->up) { + bool up = ifp->driver->up(ifp); + bool current = ifp->state != MG_TCPIP_STATE_DOWN; + if (!up && ifp->enable_dhcp_client) ifp->ip = 0; + if (up != current) { // link state has changed + ifp->state = up == false ? MG_TCPIP_STATE_DOWN + : ifp->enable_dhcp_client || ifp->ip == 0 + ? MG_TCPIP_STATE_UP + : MG_TCPIP_STATE_IP; + onstatechange(ifp); + } else if (!ifp->enable_dhcp_client && ifp->state == MG_TCPIP_STATE_UP && + ifp->ip) { + ifp->state = MG_TCPIP_STATE_IP; // ifp->fn has set an IP + onstatechange(ifp); + } + if (ifp->state == MG_TCPIP_STATE_DOWN) MG_ERROR(("Network is down")); + mg_tcpip_call(ifp, MG_TCPIP_EV_TIMER_1S, NULL); + } + if (ifp->state == MG_TCPIP_STATE_DOWN) return; -static void mg_tcpip_call(struct mg_tcpip_if *ifp, int ev, void *ev_data) { - if (ifp->fn != NULL) ifp->fn(ifp, ev, ev_data); -} + // DHCP RFC-2131 (4.4) + if (ifp->enable_dhcp_client && expired_1000ms) { + if (ifp->state == MG_TCPIP_STATE_UP) { + tx_dhcp_discover(ifp); // INIT (4.4.1) + } else if (ifp->state == MG_TCPIP_STATE_READY && + ifp->lease_expire > 0) { // BOUND / RENEWING / REBINDING + if (ifp->now >= ifp->lease_expire) { + ifp->state = MG_TCPIP_STATE_UP, ifp->ip = 0; // expired, release IP + onstatechange(ifp); + } else if (ifp->now + 30UL * 60UL * 1000UL > ifp->lease_expire && + ((ifp->now / 1000) % 60) == 0) { + // hack: 30 min before deadline, try to rebind (4.3.6) every min + tx_dhcp_request_re(ifp, (uint8_t *) broadcast, ifp->ip, 0xffffffff); + } // TODO(): Handle T1 (RENEWING) and T2 (REBINDING) (4.4.5) + } + } -static void send_syn(struct mg_connection *c); + // Read data from the network + if (ifp->driver->rx != NULL) { // Polling driver. We must call it + size_t len = + ifp->driver->rx(ifp->recv_queue.buf, ifp->recv_queue.size, ifp); + if (len > 0) { + ifp->nrecv++; + mg_tcpip_rx(ifp, ifp->recv_queue.buf, len); + } + } else { // Interrupt-based driver. Fills recv queue itself + char *buf; + size_t len = mg_queue_next(&ifp->recv_queue, &buf); + if (len > 0) { + mg_tcpip_rx(ifp, buf, len); + mg_queue_del(&ifp->recv_queue, len); + } + } -static void mkpay(struct pkt *pkt, void *p) { - pkt->pay = - mg_str_n((char *) p, (size_t) (&pkt->raw.buf[pkt->raw.len] - (char *) p)); -} + // Process timeouts + for (c = ifp->mgr->conns; c != NULL; c = c->next) { + if ((c->is_udp && !c->is_arplooking) || c->is_listening || c->is_resolving) + continue; + struct connstate *s = (struct connstate *) (c + 1); + uint32_t rem_ip; + memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); + if (now > s->timer) { + if (s->ttype == MIP_TTYPE_ARP) { + mg_error(c, "ARP timeout"); + } else if (c->is_udp) { + continue; + } else if (s->ttype == MIP_TTYPE_ACK && s->acked != s->ack) { + MG_VERBOSE(("%lu ack %x %x", c->id, s->seq, s->ack)); + tx_tcp(ifp, s->mac, rem_ip, TH_ACK, c->loc.port, c->rem.port, + mg_htonl(s->seq), mg_htonl(s->ack), NULL, 0); + s->acked = s->ack; + } else if (s->ttype == MIP_TTYPE_SYN) { + mg_error(c, "Connection timeout"); + } else if (s->ttype == MIP_TTYPE_FIN) { + c->is_closing = 1; + continue; + } else { + if (s->tmiss++ > 2) { + mg_error(c, "keepalive"); + } else { + MG_VERBOSE(("%lu keepalive", c->id)); + tx_tcp(ifp, s->mac, rem_ip, TH_ACK, c->loc.port, c->rem.port, + mg_htonl(s->seq - 1), mg_htonl(s->ack), NULL, 0); + } + } -static uint32_t csumup(uint32_t sum, const void *buf, size_t len) { - size_t i; - const uint8_t *p = (const uint8_t *) buf; - for (i = 0; i < len; i++) sum += i & 1 ? p[i] : (uint32_t) (p[i] << 8); - return sum; + settmout(c, MIP_TTYPE_KEEPALIVE); + } + } } -static uint16_t csumfin(uint32_t sum) { - while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); - return mg_htons(~sum & 0xffff); +// This function executes in interrupt context, thus it should copy data +// somewhere fast. Note that newlib's malloc is not thread safe, thus use +// our lock-free queue with preallocated buffer to copy data and return asap +void mg_tcpip_qwrite(void *buf, size_t len, struct mg_tcpip_if *ifp) { + char *p; + if (mg_queue_book(&ifp->recv_queue, &p, len) >= len) { + memcpy(p, buf, len); + mg_queue_add(&ifp->recv_queue, len); + ifp->nrecv++; + } else { + ifp->ndrop++; + } } -static uint16_t ipcsum(const void *buf, size_t len) { - uint32_t sum = csumup(0, buf, len); - return csumfin(sum); -} +void mg_tcpip_init(struct mg_mgr *mgr, struct mg_tcpip_if *ifp) { + // If MAC address is not set, make a random one + if (ifp->mac[0] == 0 && ifp->mac[1] == 0 && ifp->mac[2] == 0 && + ifp->mac[3] == 0 && ifp->mac[4] == 0 && ifp->mac[5] == 0) { + ifp->mac[0] = 0x02; // Locally administered, unicast + mg_random(&ifp->mac[1], sizeof(ifp->mac) - 1); + MG_INFO(("MAC not set. Generated random: %M", mg_print_mac, ifp->mac)); + } -static void settmout(struct mg_connection *c, uint8_t type) { - struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; - struct connstate *s = (struct connstate *) (c + 1); - unsigned n = type == MIP_TTYPE_ACK ? MIP_TCP_ACK_MS - : type == MIP_TTYPE_ARP ? MIP_ARP_RESP_MS - : type == MIP_TTYPE_SYN ? MIP_TCP_SYN_MS - : type == MIP_TTYPE_FIN ? MIP_TCP_FIN_MS - : MIP_TCP_KEEPALIVE_MS; - s->timer = ifp->now + n; - s->ttype = type; - MG_VERBOSE(("%lu %d -> %llx", c->id, type, s->timer)); + if (ifp->driver->init && !ifp->driver->init(ifp)) { + MG_ERROR(("driver init failed")); + } else { + size_t framesize = 1540; + ifp->tx.buf = (char *) calloc(1, framesize), ifp->tx.len = framesize; + if (ifp->recv_queue.size == 0) + ifp->recv_queue.size = ifp->driver->rx ? framesize : 8192; + ifp->recv_queue.buf = (char *) calloc(1, ifp->recv_queue.size); + ifp->timer_1000ms = mg_millis(); + mgr->priv = ifp; + ifp->mgr = mgr; + ifp->mtu = MG_TCPIP_MTU_DEFAULT; + mgr->extraconnsize = sizeof(struct connstate); + if (ifp->ip == 0) ifp->enable_dhcp_client = true; + memset(ifp->gwmac, 255, sizeof(ifp->gwmac)); // Set best-effort to bcast + mg_random(&ifp->eport, sizeof(ifp->eport)); // Random from 0 to 65535 + ifp->eport |= MG_EPHEMERAL_PORT_BASE; // Random from + // MG_EPHEMERAL_PORT_BASE to 65535 + if (ifp->tx.buf == NULL || ifp->recv_queue.buf == NULL) MG_ERROR(("OOM")); + } } -static size_t ether_output(struct mg_tcpip_if *ifp, size_t len) { - size_t n = ifp->driver->tx(ifp->tx.buf, len, ifp); - if (n == len) ifp->nsent++; - return n; +void mg_tcpip_free(struct mg_tcpip_if *ifp) { + free(ifp->recv_queue.buf); + free(ifp->tx.buf); } -void mg_tcpip_arp_request(struct mg_tcpip_if *ifp, uint32_t ip, uint8_t *mac) { - struct eth *eth = (struct eth *) ifp->tx.buf; - struct arp *arp = (struct arp *) (eth + 1); - memset(eth->dst, 255, sizeof(eth->dst)); - memcpy(eth->src, ifp->mac, sizeof(eth->src)); - eth->type = mg_htons(0x806); - memset(arp, 0, sizeof(*arp)); - arp->fmt = mg_htons(1), arp->pro = mg_htons(0x800), arp->hlen = 6, - arp->plen = 4; - arp->op = mg_htons(1), arp->tpa = ip, arp->spa = ifp->ip; - memcpy(arp->sha, ifp->mac, sizeof(arp->sha)); - if (mac != NULL) memcpy(arp->tha, mac, sizeof(arp->tha)); - ether_output(ifp, PDIFF(eth, arp + 1)); +static void send_syn(struct mg_connection *c) { + struct connstate *s = (struct connstate *) (c + 1); + uint32_t isn = mg_htonl((uint32_t) mg_ntohs(c->loc.port)); + struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; + uint32_t rem_ip; + memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); + tx_tcp(ifp, s->mac, rem_ip, TH_SYN, c->loc.port, c->rem.port, isn, 0, NULL, + 0); } -static void onstatechange(struct mg_tcpip_if *ifp) { - if (ifp->state == MG_TCPIP_STATE_READY) { - MG_INFO(("READY, IP: %M", mg_print_ip4, &ifp->ip)); - MG_INFO((" GW: %M", mg_print_ip4, &ifp->gw)); - MG_INFO((" MAC: %M", mg_print_mac, &ifp->mac)); - } else if (ifp->state == MG_TCPIP_STATE_IP) { - MG_ERROR(("Got IP")); - mg_tcpip_arp_request(ifp, ifp->gw, NULL); // unsolicited GW ARP request - } else if (ifp->state == MG_TCPIP_STATE_UP) { - MG_ERROR(("Link up")); - srand((unsigned int) mg_millis()); - } else if (ifp->state == MG_TCPIP_STATE_DOWN) { - MG_ERROR(("Link down")); +static void mac_resolved(struct mg_connection *c) { + if (c->is_udp) { + c->is_connecting = 0; + mg_call(c, MG_EV_CONNECT, NULL); + } else { + send_syn(c); + settmout(c, MIP_TTYPE_SYN); } - mg_tcpip_call(ifp, MG_TCPIP_EV_ST_CHG, &ifp->state); } -static struct ip *tx_ip(struct mg_tcpip_if *ifp, uint8_t *mac_dst, - uint8_t proto, uint32_t ip_src, uint32_t ip_dst, - size_t plen) { - struct eth *eth = (struct eth *) ifp->tx.buf; - struct ip *ip = (struct ip *) (eth + 1); - memcpy(eth->dst, mac_dst, sizeof(eth->dst)); - memcpy(eth->src, ifp->mac, sizeof(eth->src)); // Use our MAC - eth->type = mg_htons(0x800); - memset(ip, 0, sizeof(*ip)); - ip->ver = 0x45; // Version 4, header length 5 words - ip->frag = mg_htons(0x4000); // Don't fragment - ip->len = mg_htons((uint16_t) (sizeof(*ip) + plen)); - ip->ttl = 64; - ip->proto = proto; - ip->src = ip_src; - ip->dst = ip_dst; - ip->csum = ipcsum(ip, sizeof(*ip)); - return ip; +void mg_connect_resolved(struct mg_connection *c) { + struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; + uint32_t rem_ip; + memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); + c->is_resolving = 0; + if (ifp->eport < MG_EPHEMERAL_PORT_BASE) ifp->eport = MG_EPHEMERAL_PORT_BASE; + memcpy(c->loc.ip, &ifp->ip, sizeof(uint32_t)); + c->loc.port = mg_htons(ifp->eport++); + MG_DEBUG(("%lu %M -> %M", c->id, mg_print_ip_port, &c->loc, mg_print_ip_port, + &c->rem)); + mg_call(c, MG_EV_RESOLVE, NULL); + c->is_connecting = 1; + if (c->is_udp && (rem_ip == 0xffffffff || rem_ip == (ifp->ip | ~ifp->mask))) { + struct connstate *s = (struct connstate *) (c + 1); + memset(s->mac, 0xFF, sizeof(s->mac)); // global or local broadcast + mac_resolved(c); + } else if (ifp->ip && ((rem_ip & ifp->mask) == (ifp->ip & ifp->mask)) && + rem_ip != ifp->gw) { // skip if gw (onstatechange -> READY -> ARP) + // If we're in the same LAN, fire an ARP lookup. + MG_DEBUG(("%lu ARP lookup...", c->id)); + mg_tcpip_arp_request(ifp, rem_ip, NULL); + settmout(c, MIP_TTYPE_ARP); + c->is_arplooking = 1; + } else if ((*((uint8_t *) &rem_ip) & 0xE0) == 0xE0) { + struct connstate *s = (struct connstate *) (c + 1); // 224 to 239, E0 to EF + uint8_t mcastp[3] = {0x01, 0x00, 0x5E}; // multicast group + memcpy(s->mac, mcastp, 3); + memcpy(s->mac + 3, ((uint8_t *) &rem_ip) + 1, 3); // 23 LSb + s->mac[3] &= 0x7F; + mac_resolved(c); + } else { + struct connstate *s = (struct connstate *) (c + 1); + memcpy(s->mac, ifp->gwmac, sizeof(ifp->gwmac)); + mac_resolved(c); + } } -static void tx_udp(struct mg_tcpip_if *ifp, uint8_t *mac_dst, uint32_t ip_src, - uint16_t sport, uint32_t ip_dst, uint16_t dport, - const void *buf, size_t len) { - struct ip *ip = - tx_ip(ifp, mac_dst, 17, ip_src, ip_dst, len + sizeof(struct udp)); - struct udp *udp = (struct udp *) (ip + 1); - // MG_DEBUG(("UDP XX LEN %d %d", (int) len, (int) ifp->tx.len)); - udp->sport = sport; - udp->dport = dport; - udp->len = mg_htons((uint16_t) (sizeof(*udp) + len)); - udp->csum = 0; - uint32_t cs = csumup(0, udp, sizeof(*udp)); - cs = csumup(cs, buf, len); - cs = csumup(cs, &ip->src, sizeof(ip->src)); - cs = csumup(cs, &ip->dst, sizeof(ip->dst)); - cs += (uint32_t) (ip->proto + sizeof(*udp) + len); - udp->csum = csumfin(cs); - memmove(udp + 1, buf, len); - // MG_DEBUG(("UDP LEN %d %d", (int) len, (int) ifp->frame_len)); - ether_output(ifp, sizeof(struct eth) + sizeof(*ip) + sizeof(*udp) + len); +bool mg_open_listener(struct mg_connection *c, const char *url) { + c->loc.port = mg_htons(mg_url_port(url)); + return true; } -static void tx_dhcp(struct mg_tcpip_if *ifp, uint8_t *mac_dst, uint32_t ip_src, - uint32_t ip_dst, uint8_t *opts, size_t optslen, - bool ciaddr) { - // https://datatracker.ietf.org/doc/html/rfc2132#section-9.6 - struct dhcp dhcp = {1, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, {0}, 0, {0}}; - dhcp.magic = mg_htonl(0x63825363); - memcpy(&dhcp.hwaddr, ifp->mac, sizeof(ifp->mac)); - memcpy(&dhcp.xid, ifp->mac + 2, sizeof(dhcp.xid)); - memcpy(&dhcp.options, opts, optslen); - if (ciaddr) dhcp.ciaddr = ip_src; - tx_udp(ifp, mac_dst, ip_src, mg_htons(68), ip_dst, mg_htons(67), &dhcp, - sizeof(dhcp)); +static void write_conn(struct mg_connection *c) { + long len = c->is_tls ? mg_tls_send(c, c->send.buf, c->send.len) + : mg_io_send(c, c->send.buf, c->send.len); + if (len == MG_IO_ERR) { + mg_error(c, "tx err"); + } else if (len > 0) { + mg_iobuf_del(&c->send, 0, (size_t) len); + mg_call(c, MG_EV_WRITE, &len); + } } -static const uint8_t broadcast[] = {255, 255, 255, 255, 255, 255}; +static void init_closure(struct mg_connection *c) { + struct connstate *s = (struct connstate *) (c + 1); + if (c->is_udp == false && c->is_listening == false && + c->is_connecting == false) { // For TCP conns, + struct mg_tcpip_if *ifp = + (struct mg_tcpip_if *) c->mgr->priv; // send TCP FIN + uint32_t rem_ip; + memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); + tx_tcp(ifp, s->mac, rem_ip, TH_FIN | TH_ACK, c->loc.port, c->rem.port, + mg_htonl(s->seq), mg_htonl(s->ack), NULL, 0); + settmout(c, MIP_TTYPE_FIN); + } +} -// RFC-2131 #4.3.6, #4.4.1; RFC-2132 #9.8 -static void tx_dhcp_request_sel(struct mg_tcpip_if *ifp, uint32_t ip_req, - uint32_t ip_srv) { - uint8_t opts[] = { - 53, 1, 3, // Type: DHCP request - 12, 3, 'm', 'i', 'p', // Host name: "mip" - 54, 4, 0, 0, 0, 0, // DHCP server ID - 50, 4, 0, 0, 0, 0, // Requested IP - 55, 2, 1, 3, 255, 255, // GW, mask [DNS] [SNTP] - 255 // End of options - }; - uint8_t addopts = 0; - memcpy(opts + 10, &ip_srv, sizeof(ip_srv)); - memcpy(opts + 16, &ip_req, sizeof(ip_req)); - if (ifp->enable_req_dns) opts[24 + addopts++] = 6; // DNS - if (ifp->enable_req_sntp) opts[24 + addopts++] = 42; // SNTP - opts[21] += addopts; - tx_dhcp(ifp, (uint8_t *) broadcast, 0, 0xffffffff, opts, - sizeof(opts) + addopts - 2, false); - MG_DEBUG(("DHCP req sent")); +static void close_conn(struct mg_connection *c) { + struct connstate *s = (struct connstate *) (c + 1); + mg_iobuf_free(&s->raw); // For TLS connections, release raw data + mg_close_conn(c); } -// RFC-2131 #4.3.6, #4.4.5 (renewing: unicast, rebinding: bcast) -static void tx_dhcp_request_re(struct mg_tcpip_if *ifp, uint8_t *mac_dst, - uint32_t ip_src, uint32_t ip_dst) { - uint8_t opts[] = { - 53, 1, 3, // Type: DHCP request - 255 // End of options - }; - tx_dhcp(ifp, mac_dst, ip_src, ip_dst, opts, sizeof(opts), true); - MG_DEBUG(("DHCP req sent")); +static bool can_write(struct mg_connection *c) { + return c->is_connecting == 0 && c->is_resolving == 0 && c->send.len > 0 && + c->is_tls_hs == 0 && c->is_arplooking == 0; } -static void tx_dhcp_discover(struct mg_tcpip_if *ifp) { - uint8_t opts[] = { - 53, 1, 1, // Type: DHCP discover - 55, 2, 1, 3, // Parameters: ip, mask - 255 // End of options - }; - tx_dhcp(ifp, (uint8_t *) broadcast, 0, 0xffffffff, opts, sizeof(opts), false); - MG_DEBUG(("DHCP discover sent. Our MAC: %M", mg_print_mac, ifp->mac)); +void mg_mgr_poll(struct mg_mgr *mgr, int ms) { + struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) mgr->priv; + struct mg_connection *c, *tmp; + uint64_t now = mg_millis(); + mg_timer_poll(&mgr->timers, now); + if (ifp == NULL || ifp->driver == NULL) return; + mg_tcpip_poll(ifp, now); + for (c = mgr->conns; c != NULL; c = tmp) { + tmp = c->next; + struct connstate *s = (struct connstate *) (c + 1); + mg_call(c, MG_EV_POLL, &now); + MG_VERBOSE(("%lu .. %c%c%c%c%c", c->id, c->is_tls ? 'T' : 't', + c->is_connecting ? 'C' : 'c', c->is_tls_hs ? 'H' : 'h', + c->is_resolving ? 'R' : 'r', c->is_closing ? 'C' : 'c')); + if (c->is_tls && mg_tls_pending(c) > 0) + handle_tls_recv(c, (struct mg_iobuf *) &c->rtls); + if (can_write(c)) write_conn(c); + if (c->is_draining && c->send.len == 0 && s->ttype != MIP_TTYPE_FIN) + init_closure(c); + if (c->is_closing) close_conn(c); + } + (void) ms; } -static struct mg_connection *getpeer(struct mg_mgr *mgr, struct pkt *pkt, - bool lsn) { - struct mg_connection *c = NULL; - for (c = mgr->conns; c != NULL; c = c->next) { - if (c->is_arplooking && pkt->arp && - memcmp(&pkt->arp->spa, c->rem.ip, sizeof(pkt->arp->spa)) == 0) - break; - if (c->is_udp && pkt->udp && c->loc.port == pkt->udp->dport) break; - if (!c->is_udp && pkt->tcp && c->loc.port == pkt->tcp->dport && - lsn == c->is_listening && (lsn || c->rem.port == pkt->tcp->sport)) - break; +bool mg_send(struct mg_connection *c, const void *buf, size_t len) { + struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; + bool res = false; + uint32_t rem_ip; + memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); + if (ifp->ip == 0 || ifp->state != MG_TCPIP_STATE_READY) { + mg_error(c, "net down"); + } else if (c->is_udp && (c->is_arplooking || c->is_resolving)) { + // Fail to send, no target MAC or IP + MG_VERBOSE(("still resolving...")); + } else if (c->is_udp) { + struct connstate *s = (struct connstate *) (c + 1); + len = trim_len(c, len); // Trimming length if necessary + tx_udp(ifp, s->mac, ifp->ip, c->loc.port, rem_ip, c->rem.port, buf, len); + res = true; + } else { + res = mg_iobuf_add(&c->send, c->send.len, buf, len); } - return c; + return res; } +#endif // MG_ENABLE_TCPIP -static void mac_resolved(struct mg_connection *c); +#ifdef MG_ENABLE_LINES +#line 1 "src/ota_ch32v307.c" +#endif -static void rx_arp(struct mg_tcpip_if *ifp, struct pkt *pkt) { - if (pkt->arp->op == mg_htons(1) && pkt->arp->tpa == ifp->ip) { - // ARP request. Make a response, then send - // MG_DEBUG(("ARP op %d %M: %M", mg_ntohs(pkt->arp->op), mg_print_ip4, - // &pkt->arp->spa, mg_print_ip4, &pkt->arp->tpa)); - struct eth *eth = (struct eth *) ifp->tx.buf; - struct arp *arp = (struct arp *) (eth + 1); - memcpy(eth->dst, pkt->eth->src, sizeof(eth->dst)); - memcpy(eth->src, ifp->mac, sizeof(eth->src)); - eth->type = mg_htons(0x806); - *arp = *pkt->arp; - arp->op = mg_htons(2); - memcpy(arp->tha, pkt->arp->sha, sizeof(pkt->arp->tha)); - memcpy(arp->sha, ifp->mac, sizeof(pkt->arp->sha)); - arp->tpa = pkt->arp->spa; - arp->spa = ifp->ip; - MG_DEBUG(("ARP: tell %M we're %M", mg_print_ip4, &arp->tpa, mg_print_mac, - &ifp->mac)); - ether_output(ifp, PDIFF(eth, arp + 1)); - } else if (pkt->arp->op == mg_htons(2)) { - if (memcmp(pkt->arp->tha, ifp->mac, sizeof(pkt->arp->tha)) != 0) return; - if (pkt->arp->spa == ifp->gw) { - // Got response for the GW ARP request. Set ifp->gwmac and IP -> READY - memcpy(ifp->gwmac, pkt->arp->sha, sizeof(ifp->gwmac)); - if (ifp->state == MG_TCPIP_STATE_IP) { - ifp->state = MG_TCPIP_STATE_READY; - onstatechange(ifp); - } - } else { - struct mg_connection *c = getpeer(ifp->mgr, pkt, false); - if (c != NULL && c->is_arplooking) { - struct connstate *s = (struct connstate *) (c + 1); - memcpy(s->mac, pkt->arp->sha, sizeof(s->mac)); - MG_DEBUG(("%lu ARP resolved %M -> %M", c->id, mg_print_ip4, c->rem.ip, - mg_print_mac, s->mac)); - c->is_arplooking = 0; - mac_resolved(c); - } - } + + + +#if MG_OTA == MG_OTA_CH32V307 +// RM: https://www.wch-ic.com/downloads/CH32FV2x_V3xRM_PDF.html + +static bool mg_ch32v307_write(void *, const void *, size_t); +static bool mg_ch32v307_swap(void); + +static struct mg_flash s_mg_flash_ch32v307 = { + (void *) 0x08000000, // Start + 480 * 1024, // Size, first 320k is 0-wait + 4 * 1024, // Sector size, 4k + 4, // Align, 32 bit + mg_ch32v307_write, + mg_ch32v307_swap, +}; + +#define FLASH_BASE 0x40022000 +#define FLASH_ACTLR (FLASH_BASE + 0) +#define FLASH_KEYR (FLASH_BASE + 4) +#define FLASH_OBKEYR (FLASH_BASE + 8) +#define FLASH_STATR (FLASH_BASE + 12) +#define FLASH_CTLR (FLASH_BASE + 16) +#define FLASH_ADDR (FLASH_BASE + 20) +#define FLASH_OBR (FLASH_BASE + 28) +#define FLASH_WPR (FLASH_BASE + 32) + +MG_IRAM static void flash_unlock(void) { + static bool unlocked; + if (unlocked == false) { + MG_REG(FLASH_KEYR) = 0x45670123; + MG_REG(FLASH_KEYR) = 0xcdef89ab; + unlocked = true; } } -static void rx_icmp(struct mg_tcpip_if *ifp, struct pkt *pkt) { - // MG_DEBUG(("ICMP %d", (int) len)); - if (pkt->icmp->type == 8 && pkt->ip != NULL && pkt->ip->dst == ifp->ip) { - size_t hlen = sizeof(struct eth) + sizeof(struct ip) + sizeof(struct icmp); - size_t space = ifp->tx.len - hlen, plen = pkt->pay.len; - if (plen > space) plen = space; - struct ip *ip = tx_ip(ifp, pkt->eth->src, 1, ifp->ip, pkt->ip->src, - sizeof(struct icmp) + plen); - struct icmp *icmp = (struct icmp *) (ip + 1); - memset(icmp, 0, sizeof(*icmp)); // Set csum to 0 - memcpy(icmp + 1, pkt->pay.buf, plen); // Copy RX payload to TX - icmp->csum = ipcsum(icmp, sizeof(*icmp) + plen); - ether_output(ifp, hlen + plen); - } +MG_IRAM static void flash_wait(void) { + while (MG_REG(FLASH_STATR) & MG_BIT(0)) (void) 0; } -static void rx_dhcp_client(struct mg_tcpip_if *ifp, struct pkt *pkt) { - uint32_t ip = 0, gw = 0, mask = 0, lease = 0, dns = 0, sntp = 0; - uint8_t msgtype = 0, state = ifp->state; - // perform size check first, then access fields - uint8_t *p = pkt->dhcp->options, - *end = (uint8_t *) &pkt->raw.buf[pkt->raw.len]; - if (end < (uint8_t *) (pkt->dhcp + 1)) return; - if (memcmp(&pkt->dhcp->xid, ifp->mac + 2, sizeof(pkt->dhcp->xid))) return; - while (p + 1 < end && p[0] != 255) { // Parse options RFC-1533 #9 - if (p[0] == 1 && p[1] == sizeof(ifp->mask) && p + 6 < end) { // Mask - memcpy(&mask, p + 2, sizeof(mask)); - } else if (p[0] == 3 && p[1] == sizeof(ifp->gw) && p + 6 < end) { // GW - memcpy(&gw, p + 2, sizeof(gw)); - ip = pkt->dhcp->yiaddr; - } else if (ifp->enable_req_dns && p[0] == 6 && p[1] == sizeof(dns) && - p + 6 < end) { // DNS - memcpy(&dns, p + 2, sizeof(dns)); - } else if (ifp->enable_req_sntp && p[0] == 42 && p[1] == sizeof(sntp) && - p + 6 < end) { // SNTP - memcpy(&sntp, p + 2, sizeof(sntp)); - } else if (p[0] == 51 && p[1] == 4 && p + 6 < end) { // Lease - memcpy(&lease, p + 2, sizeof(lease)); - lease = mg_ntohl(lease); - } else if (p[0] == 53 && p[1] == 1 && p + 6 < end) { // Msg Type - msgtype = p[2]; - } - p += p[1] + 2; - } - // Process message type, RFC-1533 (9.4); RFC-2131 (3.1, 4) - if (msgtype == 6 && ifp->ip == ip) { // DHCPNACK, release IP - ifp->state = MG_TCPIP_STATE_UP, ifp->ip = 0; - } else if (msgtype == 2 && ifp->state == MG_TCPIP_STATE_UP && ip && gw && - lease) { // DHCPOFFER - // select IP, (4.4.1) (fallback to IP source addr on foul play) - tx_dhcp_request_sel(ifp, ip, - pkt->dhcp->siaddr ? pkt->dhcp->siaddr : pkt->ip->src); - ifp->state = MG_TCPIP_STATE_REQ; // REQUESTING state - } else if (msgtype == 5) { // DHCPACK - if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) { // got an IP - ifp->lease_expire = ifp->now + lease * 1000; - MG_INFO(("Lease: %u sec (%lld)", lease, ifp->lease_expire / 1000)); - // assume DHCP server = router until ARP resolves - memcpy(ifp->gwmac, pkt->eth->src, sizeof(ifp->gwmac)); - ifp->ip = ip, ifp->gw = gw, ifp->mask = mask; - ifp->state = MG_TCPIP_STATE_IP; // BOUND state - uint64_t rand; - mg_random(&rand, sizeof(rand)); - srand((unsigned int) (rand + mg_millis())); - if (ifp->enable_req_dns && dns != 0) - mg_tcpip_call(ifp, MG_TCPIP_EV_DHCP_DNS, &dns); - if (ifp->enable_req_sntp && sntp != 0) - mg_tcpip_call(ifp, MG_TCPIP_EV_DHCP_SNTP, &sntp); - } else if (ifp->state == MG_TCPIP_STATE_READY && ifp->ip == ip) { // renew - ifp->lease_expire = ifp->now + lease * 1000; - MG_INFO(("Lease: %u sec (%lld)", lease, ifp->lease_expire / 1000)); - } // TODO(): accept provided T1/T2 and store server IP for renewal (4.4) +MG_IRAM static void mg_ch32v307_erase(void *addr) { + // MG_INFO(("%p", addr)); + flash_unlock(); + flash_wait(); + MG_REG(FLASH_ADDR) = (uint32_t) addr; + MG_REG(FLASH_CTLR) |= MG_BIT(1) | MG_BIT(6); // PER | STRT; + flash_wait(); +} + +MG_IRAM static bool is_page_boundary(const void *addr) { + uint32_t val = (uint32_t) addr; + return (val & (s_mg_flash_ch32v307.secsz - 1)) == 0; +} + +MG_IRAM static bool mg_ch32v307_write(void *addr, const void *buf, size_t len) { + // MG_INFO(("%p %p %lu", addr, buf, len)); + // mg_hexdump(buf, len); + flash_unlock(); + const uint16_t *src = (uint16_t *) buf, *end = &src[len / 2]; + uint16_t *dst = (uint16_t *) addr; + MG_REG(FLASH_CTLR) |= MG_BIT(0); // Set PG + // MG_INFO(("CTLR: %#lx", MG_REG(FLASH_CTLR))); + while (src < end) { + if (is_page_boundary(dst)) mg_ch32v307_erase(dst); + *dst++ = *src++; + flash_wait(); } - if (ifp->state != state) onstatechange(ifp); + MG_REG(FLASH_CTLR) &= ~MG_BIT(0); // Clear PG + return true; } -// Simple DHCP server that assigns a next IP address: ifp->ip + 1 -static void rx_dhcp_server(struct mg_tcpip_if *ifp, struct pkt *pkt) { - uint8_t op = 0, *p = pkt->dhcp->options, - *end = (uint8_t *) &pkt->raw.buf[pkt->raw.len]; - if (end < (uint8_t *) (pkt->dhcp + 1)) return; - // struct dhcp *req = pkt->dhcp; - struct dhcp res = {2, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, {0}, 0, {0}}; - res.yiaddr = ifp->ip; - ((uint8_t *) (&res.yiaddr))[3]++; // Offer our IP + 1 - while (p + 1 < end && p[0] != 255) { // Parse options - if (p[0] == 53 && p[1] == 1 && p + 2 < end) { // Message type - op = p[2]; - } - p += p[1] + 2; +MG_IRAM bool mg_ch32v307_swap(void) { + return true; +} + +// just overwrite instead of swap +MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) { + // no stdlib calls here + for (size_t ofs = 0; ofs < s; ofs += ss) { + mg_ch32v307_write(p1 + ofs, p2 + ofs, ss); } - if (op == 1 || op == 3) { // DHCP Discover or DHCP Request - uint8_t msg = op == 1 ? 2 : 5; // Message type: DHCP OFFER or DHCP ACK - uint8_t opts[] = { - 53, 1, msg, // Message type - 1, 4, 0, 0, 0, 0, // Subnet mask - 54, 4, 0, 0, 0, 0, // Server ID - 12, 3, 'm', 'i', 'p', // Host name: "mip" - 51, 4, 255, 255, 255, 255, // Lease time - 255 // End of options - }; - memcpy(&res.hwaddr, pkt->dhcp->hwaddr, 6); - memcpy(opts + 5, &ifp->mask, sizeof(ifp->mask)); - memcpy(opts + 11, &ifp->ip, sizeof(ifp->ip)); - memcpy(&res.options, opts, sizeof(opts)); - res.magic = pkt->dhcp->magic; - res.xid = pkt->dhcp->xid; - if (ifp->enable_get_gateway) { - ifp->gw = res.yiaddr; // set gw IP, best-effort gwmac as DHCP server's - memcpy(ifp->gwmac, pkt->eth->src, sizeof(ifp->gwmac)); - } - tx_udp(ifp, pkt->eth->src, ifp->ip, mg_htons(67), - op == 1 ? ~0U : res.yiaddr, mg_htons(68), &res, sizeof(res)); + *((volatile uint32_t *) 0xbeef0000) |= 1U << 7; // NVIC_SystemReset() +} + +bool mg_ota_begin(size_t new_firmware_size) { + return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_ch32v307); +} + +bool mg_ota_write(const void *buf, size_t len) { + return mg_ota_flash_write(buf, len, &s_mg_flash_ch32v307); +} + +bool mg_ota_end(void) { + if (mg_ota_flash_end(&s_mg_flash_ch32v307)) { + // Swap partitions. Pray power does not go away + MG_INFO(("Swapping partitions, size %u (%u sectors)", + s_mg_flash_ch32v307.size, + s_mg_flash_ch32v307.size / s_mg_flash_ch32v307.secsz)); + MG_INFO(("Do NOT power off...")); + mg_log_level = MG_LL_NONE; + // TODO() disable IRQ, s_flash_irq_disabled = true; + // Runs in RAM, will reset when finished + single_bank_swap( + (char *) s_mg_flash_ch32v307.start, + (char *) s_mg_flash_ch32v307.start + s_mg_flash_ch32v307.size / 2, + s_mg_flash_ch32v307.size / 2, s_mg_flash_ch32v307.secsz); } + return false; } +#endif -static void rx_udp(struct mg_tcpip_if *ifp, struct pkt *pkt) { - struct mg_connection *c = getpeer(ifp->mgr, pkt, true); - if (c == NULL) { - // No UDP listener on this port. Should send ICMP, but keep silent. +#ifdef MG_ENABLE_LINES +#line 1 "src/ota_dummy.c" +#endif + + + +#if MG_OTA == MG_OTA_NONE +bool mg_ota_begin(size_t new_firmware_size) { + (void) new_firmware_size; + return true; +} +bool mg_ota_write(const void *buf, size_t len) { + (void) buf, (void) len; + return true; +} +bool mg_ota_end(void) { + return true; +} +#endif + +#ifdef MG_ENABLE_LINES +#line 1 "src/ota_esp32.c" +#endif + + +#if MG_ARCH == MG_ARCH_ESP32 && MG_OTA == MG_OTA_ESP32 + +static const esp_partition_t *s_ota_update_partition; +static esp_ota_handle_t s_ota_update_handle; +static bool s_ota_success; + +// Those empty macros do nothing, but mark places in the code which could +// potentially trigger a watchdog reboot due to the log flash erase operation +#define disable_wdt() +#define enable_wdt() + +bool mg_ota_begin(size_t new_firmware_size) { + if (s_ota_update_partition != NULL) { + MG_ERROR(("Update in progress. Call mg_ota_end() ?")); + return false; } else { - c->rem.port = pkt->udp->sport; - memcpy(c->rem.ip, &pkt->ip->src, sizeof(uint32_t)); - struct connstate *s = (struct connstate *) (c + 1); - memcpy(s->mac, pkt->eth->src, sizeof(s->mac)); - if (c->recv.len >= MG_MAX_RECV_SIZE) { - mg_error(c, "max_recv_buf_size reached"); - } else if (c->recv.size - c->recv.len < pkt->pay.len && - !mg_iobuf_resize(&c->recv, c->recv.len + pkt->pay.len)) { - mg_error(c, "oom"); - } else { - memcpy(&c->recv.buf[c->recv.len], pkt->pay.buf, pkt->pay.len); - c->recv.len += pkt->pay.len; - mg_call(c, MG_EV_READ, &pkt->pay.len); - } + s_ota_success = false; + disable_wdt(); + s_ota_update_partition = esp_ota_get_next_update_partition(NULL); + esp_err_t err = esp_ota_begin(s_ota_update_partition, new_firmware_size, + &s_ota_update_handle); + enable_wdt(); + MG_DEBUG(("esp_ota_begin(): %d", err)); + s_ota_success = (err == ESP_OK); + } + return s_ota_success; +} + +bool mg_ota_write(const void *buf, size_t len) { + disable_wdt(); + esp_err_t err = esp_ota_write(s_ota_update_handle, buf, len); + enable_wdt(); + MG_INFO(("esp_ota_write(): %d", err)); + s_ota_success = err == ESP_OK; + return s_ota_success; +} + +bool mg_ota_end(void) { + esp_err_t err = esp_ota_end(s_ota_update_handle); + MG_DEBUG(("esp_ota_end(%p): %d", s_ota_update_handle, err)); + if (s_ota_success && err == ESP_OK) { + err = esp_ota_set_boot_partition(s_ota_update_partition); + s_ota_success = (err == ESP_OK); } + MG_DEBUG(("Finished ESP32 OTA, success: %d", s_ota_success)); + s_ota_update_partition = NULL; + return s_ota_success; } -static size_t tx_tcp(struct mg_tcpip_if *ifp, uint8_t *dst_mac, uint32_t dst_ip, - uint8_t flags, uint16_t sport, uint16_t dport, - uint32_t seq, uint32_t ack, const void *buf, size_t len) { -#if 0 - uint8_t opts[] = {2, 4, 5, 0xb4, 4, 2, 0, 0}; // MSS = 1460, SACK permitted - if (flags & TH_SYN) { - // Handshake? Set MSS - buf = opts; - len = sizeof(opts); +#endif + +#ifdef MG_ENABLE_LINES +#line 1 "src/ota_imxrt.c" +#endif + + + + +#if MG_OTA >= MG_OTA_RT1020 && MG_OTA <= MG_OTA_RT1170 + +static bool mg_imxrt_write(void *, const void *, size_t); +static bool mg_imxrt_swap(void); + +#if MG_OTA <= MG_OTA_RT1060 +#define MG_IMXRT_FLASH_START 0x60000000 +#define FLEXSPI_NOR_INSTANCE 0 +#elif MG_OTA == MG_OTA_RT1064 +#define MG_IMXRT_FLASH_START 0x70000000 +#define FLEXSPI_NOR_INSTANCE 1 +#else // RT1170 +#define MG_IMXRT_FLASH_START 0x30000000 +#define FLEXSPI_NOR_INSTANCE 1 +#endif + +// TODO(): fill at init, support more devices in a dynamic way +// TODO(): then, check alignment is <= 256, see Wizard's #251 +static struct mg_flash s_mg_flash_imxrt = { + (void *) MG_IMXRT_FLASH_START, // Start, + 4 * 1024 * 1024, // Size, 4mb + 4 * 1024, // Sector size, 4k + 256, // Align, + mg_imxrt_write, + mg_imxrt_swap, +}; + +struct mg_flexspi_lut_seq { + uint8_t seqNum; + uint8_t seqId; + uint16_t reserved; +}; + +struct mg_flexspi_mem_config { + uint32_t tag; + uint32_t version; + uint32_t reserved0; + uint8_t readSampleClkSrc; + uint8_t csHoldTime; + uint8_t csSetupTime; + uint8_t columnAddressWidth; + uint8_t deviceModeCfgEnable; + uint8_t deviceModeType; + uint16_t waitTimeCfgCommands; + struct mg_flexspi_lut_seq deviceModeSeq; + uint32_t deviceModeArg; + uint8_t configCmdEnable; + uint8_t configModeType[3]; + struct mg_flexspi_lut_seq configCmdSeqs[3]; + uint32_t reserved1; + uint32_t configCmdArgs[3]; + uint32_t reserved2; + uint32_t controllerMiscOption; + uint8_t deviceType; + uint8_t sflashPadType; + uint8_t serialClkFreq; + uint8_t lutCustomSeqEnable; + uint32_t reserved3[2]; + uint32_t sflashA1Size; + uint32_t sflashA2Size; + uint32_t sflashB1Size; + uint32_t sflashB2Size; + uint32_t csPadSettingOverride; + uint32_t sclkPadSettingOverride; + uint32_t dataPadSettingOverride; + uint32_t dqsPadSettingOverride; + uint32_t timeoutInMs; + uint32_t commandInterval; + uint16_t dataValidTime[2]; + uint16_t busyOffset; + uint16_t busyBitPolarity; + uint32_t lookupTable[64]; + struct mg_flexspi_lut_seq lutCustomSeq[12]; + uint32_t reserved4[4]; +}; + +struct mg_flexspi_nor_config { + struct mg_flexspi_mem_config memConfig; + uint32_t pageSize; + uint32_t sectorSize; + uint8_t ipcmdSerialClkFreq; + uint8_t isUniformBlockSize; + uint8_t reserved0[2]; + uint8_t serialNorType; + uint8_t needExitNoCmdMode; + uint8_t halfClkForNonReadCmd; + uint8_t needRestoreNoCmdMode; + uint32_t blockSize; + uint32_t reserve2[11]; +}; + +/* FLEXSPI memory config block related defintions */ +#define MG_FLEXSPI_CFG_BLK_TAG (0x42464346UL) // ascii "FCFB" Big Endian +#define MG_FLEXSPI_CFG_BLK_VERSION (0x56010400UL) // V1.4.0 + +#define MG_FLEXSPI_LUT_SEQ(cmd0, pad0, op0, cmd1, pad1, op1) \ + (MG_FLEXSPI_LUT_OPERAND0(op0) | MG_FLEXSPI_LUT_NUM_PADS0(pad0) | \ + MG_FLEXSPI_LUT_OPCODE0(cmd0) | MG_FLEXSPI_LUT_OPERAND1(op1) | \ + MG_FLEXSPI_LUT_NUM_PADS1(pad1) | MG_FLEXSPI_LUT_OPCODE1(cmd1)) + +#define MG_CMD_SDR 0x01 +#define MG_CMD_DDR 0x21 +#define MG_DUMMY_SDR 0x0C +#define MG_DUMMY_DDR 0x2C +#define MG_RADDR_SDR 0x02 +#define MG_RADDR_DDR 0x22 +#define MG_READ_SDR 0x09 +#define MG_READ_DDR 0x29 +#define MG_WRITE_SDR 0x08 +#define MG_WRITE_DDR 0x28 +#define MG_STOP 0 + +#define MG_FLEXSPI_1PAD 0 +#define MG_FLEXSPI_2PAD 1 +#define MG_FLEXSPI_4PAD 2 +#define MG_FLEXSPI_8PAD 3 + +#define MG_FLEXSPI_QSPI_LUT \ + { \ + [0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0xEB, MG_RADDR_SDR, \ + MG_FLEXSPI_4PAD, 0x18), \ + [1] = MG_FLEXSPI_LUT_SEQ(MG_DUMMY_SDR, MG_FLEXSPI_4PAD, 0x06, MG_READ_SDR, \ + MG_FLEXSPI_4PAD, 0x04), \ + [4 * 1 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x05, \ + MG_READ_SDR, MG_FLEXSPI_1PAD, 0x04), \ + [4 * 3 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x06, \ + MG_STOP, MG_FLEXSPI_1PAD, 0x0), \ + [4 * 5 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x20, \ + MG_RADDR_SDR, MG_FLEXSPI_1PAD, 0x18), \ + [4 * 8 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0xD8, \ + MG_RADDR_SDR, MG_FLEXSPI_1PAD, 0x18), \ + [4 * 9 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x02, \ + MG_RADDR_SDR, MG_FLEXSPI_1PAD, 0x18), \ + [4 * 9 + 1] = MG_FLEXSPI_LUT_SEQ(MG_WRITE_SDR, MG_FLEXSPI_1PAD, 0x04, \ + MG_STOP, MG_FLEXSPI_1PAD, 0x0), \ + [4 * 11 + 0] = MG_FLEXSPI_LUT_SEQ(MG_CMD_SDR, MG_FLEXSPI_1PAD, 0x60, \ + MG_STOP, MG_FLEXSPI_1PAD, 0x0), \ } + +#define MG_FLEXSPI_LUT_OPERAND0(x) (((uint32_t) (((uint32_t) (x)))) & 0xFFU) +#define MG_FLEXSPI_LUT_NUM_PADS0(x) \ + (((uint32_t) (((uint32_t) (x)) << 8U)) & 0x300U) +#define MG_FLEXSPI_LUT_OPCODE0(x) \ + (((uint32_t) (((uint32_t) (x)) << 10U)) & 0xFC00U) +#define MG_FLEXSPI_LUT_OPERAND1(x) \ + (((uint32_t) (((uint32_t) (x)) << 16U)) & 0xFF0000U) +#define MG_FLEXSPI_LUT_NUM_PADS1(x) \ + (((uint32_t) (((uint32_t) (x)) << 24U)) & 0x3000000U) +#define MG_FLEXSPI_LUT_OPCODE1(x) \ + (((uint32_t) (((uint32_t) (x)) << 26U)) & 0xFC000000U) + +#if MG_OTA == MG_OTA_RT1020 +// RT102X boards support ROM API version 1.4 +struct mg_flexspi_nor_driver_interface { + uint32_t version; + int (*init)(uint32_t instance, struct mg_flexspi_nor_config *config); + int (*program)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t dst_addr, const uint32_t *src); + uint32_t reserved; + int (*erase)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t start, uint32_t lengthInBytes); + uint32_t reserved2; + int (*update_lut)(uint32_t instance, uint32_t seqIndex, + const uint32_t *lutBase, uint32_t seqNumber); + int (*xfer)(uint32_t instance, char *xfer); + void (*clear_cache)(uint32_t instance); +}; +#elif MG_OTA <= MG_OTA_RT1064 +// RT104x and RT106x support ROM API version 1.5 +struct mg_flexspi_nor_driver_interface { + uint32_t version; + int (*init)(uint32_t instance, struct mg_flexspi_nor_config *config); + int (*program)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t dst_addr, const uint32_t *src); + int (*erase_all)(uint32_t instance, struct mg_flexspi_nor_config *config); + int (*erase)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t start, uint32_t lengthInBytes); + int (*read)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t *dst, uint32_t addr, uint32_t lengthInBytes); + void (*clear_cache)(uint32_t instance); + int (*xfer)(uint32_t instance, char *xfer); + int (*update_lut)(uint32_t instance, uint32_t seqIndex, + const uint32_t *lutBase, uint32_t seqNumber); + int (*get_config)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t *option); +}; +#else +// RT117x support ROM API version 1.7 +struct mg_flexspi_nor_driver_interface { + uint32_t version; + int (*init)(uint32_t instance, struct mg_flexspi_nor_config *config); + int (*program)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t dst_addr, const uint32_t *src); + int (*erase_all)(uint32_t instance, struct mg_flexspi_nor_config *config); + int (*erase)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t start, uint32_t lengthInBytes); + int (*read)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t *dst, uint32_t addr, uint32_t lengthInBytes); + uint32_t reserved; + int (*xfer)(uint32_t instance, char *xfer); + int (*update_lut)(uint32_t instance, uint32_t seqIndex, + const uint32_t *lutBase, uint32_t seqNumber); + int (*get_config)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t *option); + int (*erase_sector)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t address); + int (*erase_block)(uint32_t instance, struct mg_flexspi_nor_config *config, + uint32_t address); + void (*hw_reset)(uint32_t instance, uint32_t resetLogic); + int (*wait_busy)(uint32_t instance, struct mg_flexspi_nor_config *config, + bool isParallelMode, uint32_t address); + int (*set_clock_source)(uint32_t instance, uint32_t clockSrc); + void (*config_clock)(uint32_t instance, uint32_t freqOption, + uint32_t sampleClkMode); +}; #endif - struct ip *ip = - tx_ip(ifp, dst_mac, 6, ifp->ip, dst_ip, sizeof(struct tcp) + len); - struct tcp *tcp = (struct tcp *) (ip + 1); - memset(tcp, 0, sizeof(*tcp)); - if (buf != NULL && len) memmove(tcp + 1, buf, len); - tcp->sport = sport; - tcp->dport = dport; - tcp->seq = seq; - tcp->ack = ack; - tcp->flags = flags; - tcp->win = mg_htons(MIP_TCP_WIN); - tcp->off = (uint8_t) (sizeof(*tcp) / 4 << 4); - // if (flags & TH_SYN) tcp->off = 0x70; // Handshake? header size 28 bytes - uint32_t cs = 0; - uint16_t n = (uint16_t) (sizeof(*tcp) + len); - uint8_t pseudo[] = {0, ip->proto, (uint8_t) (n >> 8), (uint8_t) (n & 255)}; - cs = csumup(cs, tcp, n); - cs = csumup(cs, &ip->src, sizeof(ip->src)); - cs = csumup(cs, &ip->dst, sizeof(ip->dst)); - cs = csumup(cs, pseudo, sizeof(pseudo)); - tcp->csum = csumfin(cs); - MG_VERBOSE(("TCP %M:%hu -> %M:%hu fl %x len %u", mg_print_ip4, &ip->src, - mg_ntohs(tcp->sport), mg_print_ip4, &ip->dst, - mg_ntohs(tcp->dport), tcp->flags, len)); - // mg_hexdump(ifp->tx.buf, PDIFF(ifp->tx.buf, tcp + 1) + len); - return ether_output(ifp, PDIFF(ifp->tx.buf, tcp + 1) + len); -} +#if MG_OTA <= MG_OTA_RT1064 +#define MG_FLEXSPI_BASE 0x402A8000 +#define flexspi_nor \ + (*((struct mg_flexspi_nor_driver_interface **) (*(uint32_t *) 0x0020001c + \ + 16))) +#else +#define MG_FLEXSPI_BASE 0x400CC000 +#define flexspi_nor \ + (*((struct mg_flexspi_nor_driver_interface **) (*(uint32_t *) 0x0021001c + \ + 12))) +#endif -static size_t tx_tcp_pkt(struct mg_tcpip_if *ifp, struct pkt *pkt, - uint8_t flags, uint32_t seq, const void *buf, - size_t len) { - uint32_t delta = (pkt->tcp->flags & (TH_SYN | TH_FIN)) ? 1 : 0; - return tx_tcp(ifp, pkt->eth->src, pkt->ip->src, flags, pkt->tcp->dport, - pkt->tcp->sport, seq, mg_htonl(mg_ntohl(pkt->tcp->seq) + delta), - buf, len); +static bool s_flash_irq_disabled; + +MG_IRAM static bool flash_page_start(volatile uint32_t *dst) { + char *base = (char *) s_mg_flash_imxrt.start, *end = base + s_mg_flash_imxrt.size; + volatile char *p = (char *) dst; + return p >= base && p < end && ((p - base) % s_mg_flash_imxrt.secsz) == 0; } -static struct mg_connection *accept_conn(struct mg_connection *lsn, - struct pkt *pkt) { - struct mg_connection *c = mg_alloc_conn(lsn->mgr); - if (c == NULL) { - MG_ERROR(("OOM")); - return NULL; - } - struct connstate *s = (struct connstate *) (c + 1); - s->seq = mg_ntohl(pkt->tcp->ack), s->ack = mg_ntohl(pkt->tcp->seq); - memcpy(s->mac, pkt->eth->src, sizeof(s->mac)); - settmout(c, MIP_TTYPE_KEEPALIVE); - memcpy(c->rem.ip, &pkt->ip->src, sizeof(uint32_t)); - c->rem.port = pkt->tcp->sport; - MG_DEBUG(("%lu accepted %M", c->id, mg_print_ip_port, &c->rem)); - LIST_ADD_HEAD(struct mg_connection, &lsn->mgr->conns, c); - c->is_accepted = 1; - c->is_hexdumping = lsn->is_hexdumping; - c->pfn = lsn->pfn; - c->loc = lsn->loc; - c->pfn_data = lsn->pfn_data; - c->fn = lsn->fn; - c->fn_data = lsn->fn_data; - mg_call(c, MG_EV_OPEN, NULL); - mg_call(c, MG_EV_ACCEPT, NULL); - return c; +// Note: the get_config function below works both for RT1020 and 1060 +// must reside in RAM, as flash will be erased +static struct mg_flexspi_nor_config default_config = { + .memConfig = {.tag = MG_FLEXSPI_CFG_BLK_TAG, + .version = MG_FLEXSPI_CFG_BLK_VERSION, + .readSampleClkSrc = 1, // ReadSampleClk_LoopbackFromDqsPad + .csHoldTime = 3, + .csSetupTime = 3, + .controllerMiscOption = MG_BIT(4), + .deviceType = 1, // serial NOR + .sflashPadType = 4, + .serialClkFreq = 7, // 133MHz + .sflashA1Size = 8 * 1024 * 1024, + .lookupTable = MG_FLEXSPI_QSPI_LUT}, + .pageSize = 256, + .sectorSize = 4 * 1024, + .ipcmdSerialClkFreq = 1, + .blockSize = 64 * 1024, + .isUniformBlockSize = false +}; +MG_IRAM static int flexspi_nor_get_config( + struct mg_flexspi_nor_config **config) { + *config = &default_config; + return 0; } -static size_t trim_len(struct mg_connection *c, size_t len) { - struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; - size_t eth_h_len = 14, ip_max_h_len = 24, tcp_max_h_len = 60, udp_h_len = 8; - size_t max_headers_len = - eth_h_len + ip_max_h_len + (c->is_udp ? udp_h_len : tcp_max_h_len); - size_t min_mtu = c->is_udp ? 68 /* RFC-791 */ : max_headers_len - eth_h_len; +#if 0 +// ROM API get_config call (ROM version >= 1.5) +MG_IRAM static int flexspi_nor_get_config( + struct mg_flexspi_nor_config **config) { + uint32_t options[] = {0xc0000000, 0x00}; - // If the frame exceeds the available buffer, trim the length - if (len + max_headers_len > ifp->tx.len) { - len = ifp->tx.len - max_headers_len; - } - // Ensure the MTU isn't lower than the minimum allowed value - if (ifp->mtu < min_mtu) { - MG_ERROR(("MTU is lower than minimum, capping to %lu", min_mtu)); - ifp->mtu = (uint16_t) min_mtu; + MG_ARM_DISABLE_IRQ(); + uint32_t status = + flexspi_nor->get_config(FLEXSPI_NOR_INSTANCE, *config, options); + if (!s_flash_irq_disabled) { + MG_ARM_ENABLE_IRQ(); } - // If the total packet size exceeds the MTU, trim the length - if (len + max_headers_len - eth_h_len > ifp->mtu) { - len = ifp->mtu - max_headers_len + eth_h_len; - if (c->is_udp) { - MG_ERROR(("UDP datagram exceeds MTU. Truncating it.")); - } + if (status) { + MG_ERROR(("Failed to extract flash configuration: status %u", status)); } - - return len; + return status; } +#endif -long mg_io_send(struct mg_connection *c, const void *buf, size_t len) { - struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; - struct connstate *s = (struct connstate *) (c + 1); - uint32_t dst_ip = *(uint32_t *) c->rem.ip; - len = trim_len(c, len); - if (c->is_udp) { - tx_udp(ifp, s->mac, ifp->ip, c->loc.port, dst_ip, c->rem.port, buf, len); - } else { - size_t sent = - tx_tcp(ifp, s->mac, dst_ip, TH_PUSH | TH_ACK, c->loc.port, c->rem.port, - mg_htonl(s->seq), mg_htonl(s->ack), buf, len); - if (sent == 0) { - return MG_IO_WAIT; - } else if (sent == (size_t) -1) { - return MG_IO_ERR; - } else { - s->seq += (uint32_t) len; - if (s->ttype == MIP_TTYPE_ACK) settmout(c, MIP_TTYPE_KEEPALIVE); - } - } - return (long) len; +MG_IRAM static void mg_spin(volatile uint32_t count) { + while (count--) (void) 0; } -static void handle_tls_recv(struct mg_connection *c, struct mg_iobuf *io) { - long n = mg_tls_recv(c, &io->buf[io->len], io->size - io->len); - if (n == MG_IO_ERR) { - mg_error(c, "TLS recv error"); - } else if (n > 0) { - // Decrypted successfully - trigger MG_EV_READ - io->len += (size_t) n; - mg_call(c, MG_EV_READ, &n); - } +MG_IRAM static void flash_wait(void) { + while ((*((volatile uint32_t *) (MG_FLEXSPI_BASE + 0xE0)) & MG_BIT(1)) == 0) + mg_spin(1); } -static void read_conn(struct mg_connection *c, struct pkt *pkt) { - struct connstate *s = (struct connstate *) (c + 1); - struct mg_iobuf *io = c->is_tls ? &c->rtls : &c->recv; - uint32_t seq = mg_ntohl(pkt->tcp->seq); - uint32_t rem_ip; - memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); - if (pkt->tcp->flags & TH_FIN) { - // If we initiated the closure, we reply with ACK upon receiving FIN - // If we didn't initiate it, we reply with FIN as part of the normal TCP - // closure process - uint8_t flags = TH_ACK; - s->ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len + 1); - if (c->is_draining && s->ttype == MIP_TTYPE_FIN) { - if (s->seq == mg_htonl(pkt->tcp->ack)) { // Simultaneous closure ? - s->seq++; // Yes. Increment our SEQ - } else { // Otherwise, - s->seq = mg_htonl(pkt->tcp->ack); // Set to peer's ACK - } - } else { - flags |= TH_FIN; - c->is_draining = 1; - settmout(c, MIP_TTYPE_FIN); - } - tx_tcp((struct mg_tcpip_if *) c->mgr->priv, s->mac, rem_ip, flags, - c->loc.port, c->rem.port, mg_htonl(s->seq), mg_htonl(s->ack), "", 0); - } else if (pkt->pay.len == 0) { - // TODO(cpq): handle this peer's ACK - } else if (seq != s->ack) { - uint32_t ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len); - if (s->ack == ack) { - MG_VERBOSE(("ignoring duplicate pkt")); - } else { - MG_VERBOSE(("SEQ != ACK: %x %x %x", seq, s->ack, ack)); - tx_tcp((struct mg_tcpip_if *) c->mgr->priv, s->mac, rem_ip, TH_ACK, - c->loc.port, c->rem.port, mg_htonl(s->seq), mg_htonl(s->ack), "", - 0); - } - } else if (io->size - io->len < pkt->pay.len && - !mg_iobuf_resize(io, io->len + pkt->pay.len)) { - mg_error(c, "oom"); - } else { - // Copy TCP payload into the IO buffer. If the connection is plain text, - // we copy to c->recv. If the connection is TLS, this data is encrypted, - // therefore we copy that encrypted data to the c->rtls iobuffer instead, - // and then call mg_tls_recv() to decrypt it. NOTE: mg_tls_recv() will - // call back mg_io_recv() which grabs raw data from c->rtls - memcpy(&io->buf[io->len], pkt->pay.buf, pkt->pay.len); - io->len += pkt->pay.len; - - MG_VERBOSE(("%lu SEQ %x -> %x", c->id, mg_htonl(pkt->tcp->seq), s->ack)); - // Advance ACK counter - s->ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len); - s->unacked += pkt->pay.len; - // size_t diff = s->acked <= s->ack ? s->ack - s->acked : s->ack; - if (s->unacked > MIP_TCP_WIN / 2 && s->acked != s->ack) { - // Send ACK immediately - MG_VERBOSE(("%lu imm ACK %lu", c->id, s->acked)); - tx_tcp((struct mg_tcpip_if *) c->mgr->priv, s->mac, rem_ip, TH_ACK, - c->loc.port, c->rem.port, mg_htonl(s->seq), mg_htonl(s->ack), NULL, - 0); - s->unacked = 0; - s->acked = s->ack; - if (s->ttype != MIP_TTYPE_KEEPALIVE) settmout(c, MIP_TTYPE_KEEPALIVE); - } else { - // if not already running, setup a timer to send an ACK later - if (s->ttype != MIP_TTYPE_ACK) settmout(c, MIP_TTYPE_ACK); - } - - if (c->is_tls && c->is_tls_hs) { - mg_tls_handshake(c); - } else if (c->is_tls) { - // TLS connection. Make room for decrypted data in c->recv - io = &c->recv; - if (io->size - io->len < pkt->pay.len && - !mg_iobuf_resize(io, io->len + pkt->pay.len)) { - mg_error(c, "oom"); - } else { - // Decrypt data directly into c->recv - handle_tls_recv(c, io); - } - } else { - // Plain text connection, data is already in c->recv, trigger - // MG_EV_READ - mg_call(c, MG_EV_READ, &pkt->pay.len); - } +MG_IRAM static bool flash_erase(struct mg_flexspi_nor_config *config, + void *addr) { + if (flash_page_start(addr) == false) { + MG_ERROR(("%p is not on a sector boundary", addr)); + return false; } + + void *dst = (void *) ((char *) addr - (char *) s_mg_flash_imxrt.start); + + bool ok = (flexspi_nor->erase(FLEXSPI_NOR_INSTANCE, config, (uint32_t) dst, + s_mg_flash_imxrt.secsz) == 0); + MG_DEBUG(("Sector starting at %p erasure: %s", addr, ok ? "ok" : "fail")); + return ok; } -static void rx_tcp(struct mg_tcpip_if *ifp, struct pkt *pkt) { - struct mg_connection *c = getpeer(ifp->mgr, pkt, false); - struct connstate *s = c == NULL ? NULL : (struct connstate *) (c + 1); -#if 0 - MG_INFO(("%lu %hhu %d", c ? c->id : 0, pkt->tcp->flags, (int) pkt->pay.len)); -#endif - if (c != NULL && c->is_connecting && pkt->tcp->flags == (TH_SYN | TH_ACK)) { - s->seq = mg_ntohl(pkt->tcp->ack), s->ack = mg_ntohl(pkt->tcp->seq) + 1; - tx_tcp_pkt(ifp, pkt, TH_ACK, pkt->tcp->ack, NULL, 0); - c->is_connecting = 0; // Client connected - settmout(c, MIP_TTYPE_KEEPALIVE); - mg_call(c, MG_EV_CONNECT, NULL); // Let user know - if (c->is_tls_hs) mg_tls_handshake(c); - } else if (c != NULL && c->is_connecting && pkt->tcp->flags != TH_ACK) { - // mg_hexdump(pkt->raw.buf, pkt->raw.len); - tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0); - } else if (c != NULL && pkt->tcp->flags & TH_RST) { - mg_error(c, "peer RST"); // RFC-1122 4.2.2.13 - } else if (c != NULL) { #if 0 - MG_DEBUG(("%lu %d %M:%hu -> %M:%hu", c->id, (int) pkt->raw.len, - mg_print_ip4, &pkt->ip->src, mg_ntohs(pkt->tcp->sport), - mg_print_ip4, &pkt->ip->dst, mg_ntohs(pkt->tcp->dport))); - mg_hexdump(pkt->pay.buf, pkt->pay.len); +// standalone erase call +MG_IRAM static bool mg_imxrt_erase(void *addr) { + struct mg_flexspi_nor_config config, *config_ptr = &config; + bool ret; + // Interrupts must be disabled before calls to ROM API in RT1020 and 1060 + MG_ARM_DISABLE_IRQ(); + ret = (flexspi_nor_get_config(&config_ptr) == 0); + if (ret) ret = flash_erase(config_ptr, addr); + MG_ARM_ENABLE_IRQ(); + return ret; +} #endif - s->tmiss = 0; // Reset missed keep-alive counter - if (s->ttype == MIP_TTYPE_KEEPALIVE) // Advance keep-alive timer - settmout(c, - MIP_TTYPE_KEEPALIVE); // unless a former ACK timeout is pending - read_conn(c, pkt); // Override timer with ACK timeout if needed - } else if ((c = getpeer(ifp->mgr, pkt, true)) == NULL) { - tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0); - } else if (pkt->tcp->flags & TH_RST) { - if (c->is_accepted) mg_error(c, "peer RST"); // RFC-1122 4.2.2.13 - // ignore RST if not connected - } else if (pkt->tcp->flags & TH_SYN) { - // Use peer's source port as ISN, in order to recognise the handshake - uint32_t isn = mg_htonl((uint32_t) mg_ntohs(pkt->tcp->sport)); - tx_tcp_pkt(ifp, pkt, TH_SYN | TH_ACK, isn, NULL, 0); - } else if (pkt->tcp->flags & TH_FIN) { - tx_tcp_pkt(ifp, pkt, TH_FIN | TH_ACK, pkt->tcp->ack, NULL, 0); - } else if (mg_htonl(pkt->tcp->ack) == mg_htons(pkt->tcp->sport) + 1U) { - accept_conn(c, pkt); - } else if (!c->is_accepted) { // no peer - tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0); - } else { - // MG_VERBOSE(("dropped silently..")); - } + +MG_IRAM bool mg_imxrt_swap(void) { + return true; } -static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) { - uint16_t frag = mg_ntohs(pkt->ip->frag); - if (frag & IP_MORE_FRAGS_MSK || frag & IP_FRAG_OFFSET_MSK) { - if (pkt->ip->proto == 17) pkt->udp = (struct udp *) (pkt->ip + 1); - if (pkt->ip->proto == 6) pkt->tcp = (struct tcp *) (pkt->ip + 1); - struct mg_connection *c = getpeer(ifp->mgr, pkt, false); - if (c) mg_error(c, "Received fragmented packet"); - } else if (pkt->ip->proto == 1) { - pkt->icmp = (struct icmp *) (pkt->ip + 1); - if (pkt->pay.len < sizeof(*pkt->icmp)) return; - mkpay(pkt, pkt->icmp + 1); - rx_icmp(ifp, pkt); - } else if (pkt->ip->proto == 17) { - pkt->udp = (struct udp *) (pkt->ip + 1); - if (pkt->pay.len < sizeof(*pkt->udp)) return; - mkpay(pkt, pkt->udp + 1); - MG_VERBOSE(("UDP %M:%hu -> %M:%hu len %u", mg_print_ip4, &pkt->ip->src, - mg_ntohs(pkt->udp->sport), mg_print_ip4, &pkt->ip->dst, - mg_ntohs(pkt->udp->dport), (int) pkt->pay.len)); - if (ifp->enable_dhcp_client && pkt->udp->dport == mg_htons(68)) { - pkt->dhcp = (struct dhcp *) (pkt->udp + 1); - mkpay(pkt, pkt->dhcp + 1); - rx_dhcp_client(ifp, pkt); - } else if (ifp->enable_dhcp_server && pkt->udp->dport == mg_htons(67)) { - pkt->dhcp = (struct dhcp *) (pkt->udp + 1); - mkpay(pkt, pkt->dhcp + 1); - rx_dhcp_server(ifp, pkt); +MG_IRAM static bool mg_imxrt_write(void *addr, const void *buf, size_t len) { + struct mg_flexspi_nor_config config, *config_ptr = &config; + bool ok = false; + // Interrupts must be disabled before calls to ROM API in RT1020 and 1060 + MG_ARM_DISABLE_IRQ(); + if (flexspi_nor_get_config(&config_ptr) != 0) goto fwxit; + if ((len % s_mg_flash_imxrt.align) != 0) { + MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_imxrt.align)); + goto fwxit; + } + if ((char *) addr < (char *) s_mg_flash_imxrt.start) { + MG_ERROR(("Invalid flash write address: %p", addr)); + goto fwxit; + } + + uint32_t *dst = (uint32_t *) addr; + uint32_t *src = (uint32_t *) buf; + uint32_t *end = (uint32_t *) ((char *) buf + len); + ok = true; + + while (ok && src < end) { + if (flash_page_start(dst) && flash_erase(config_ptr, dst) == false) { + ok = false; + break; + } + uint32_t status; + uint32_t dst_ofs = (uint32_t) dst - (uint32_t) s_mg_flash_imxrt.start; + if ((char *) buf >= (char *) s_mg_flash_imxrt.start) { + // If we copy from FLASH to FLASH, then we first need to copy the source + // to RAM + size_t tmp_buf_size = s_mg_flash_imxrt.align / sizeof(uint32_t); + uint32_t tmp[tmp_buf_size]; + + for (size_t i = 0; i < tmp_buf_size; i++) { + flash_wait(); + tmp[i] = src[i]; + } + status = flexspi_nor->program(FLEXSPI_NOR_INSTANCE, config_ptr, + (uint32_t) dst_ofs, tmp); } else { - rx_udp(ifp, pkt); + status = flexspi_nor->program(FLEXSPI_NOR_INSTANCE, config_ptr, + (uint32_t) dst_ofs, src); + } + src = (uint32_t *) ((char *) src + s_mg_flash_imxrt.align); + dst = (uint32_t *) ((char *) dst + s_mg_flash_imxrt.align); + if (status != 0) { + ok = false; } - } else if (pkt->ip->proto == 6) { - pkt->tcp = (struct tcp *) (pkt->ip + 1); - if (pkt->pay.len < sizeof(*pkt->tcp)) return; - mkpay(pkt, pkt->tcp + 1); - uint16_t iplen = mg_ntohs(pkt->ip->len); - uint16_t off = (uint16_t) (sizeof(*pkt->ip) + ((pkt->tcp->off >> 4) * 4U)); - if (iplen >= off) pkt->pay.len = (size_t) (iplen - off); - MG_VERBOSE(("TCP %M:%hu -> %M:%hu len %u", mg_print_ip4, &pkt->ip->src, - mg_ntohs(pkt->tcp->sport), mg_print_ip4, &pkt->ip->dst, - mg_ntohs(pkt->tcp->dport), (int) pkt->pay.len)); - rx_tcp(ifp, pkt); } + MG_DEBUG(("Flash write %lu bytes @ %p: %s.", len, dst, ok ? "ok" : "fail")); +fwxit: + if (!s_flash_irq_disabled) MG_ARM_ENABLE_IRQ(); + return ok; } -static void rx_ip6(struct mg_tcpip_if *ifp, struct pkt *pkt) { - // MG_DEBUG(("IP %d", (int) len)); - if (pkt->ip6->proto == 1 || pkt->ip6->proto == 58) { - pkt->icmp = (struct icmp *) (pkt->ip6 + 1); - if (pkt->pay.len < sizeof(*pkt->icmp)) return; - mkpay(pkt, pkt->icmp + 1); - rx_icmp(ifp, pkt); - } else if (pkt->ip6->proto == 17) { - pkt->udp = (struct udp *) (pkt->ip6 + 1); - if (pkt->pay.len < sizeof(*pkt->udp)) return; - // MG_DEBUG((" UDP %u %u -> %u", len, mg_htons(udp->sport), - // mg_htons(udp->dport))); - mkpay(pkt, pkt->udp + 1); +// just overwrite instead of swap +MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) { + // no stdlib calls here + for (size_t ofs = 0; ofs < s; ofs += ss) { + mg_imxrt_write(p1 + ofs, p2 + ofs, ss); } + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; } -static void mg_tcpip_rx(struct mg_tcpip_if *ifp, void *buf, size_t len) { - struct pkt pkt; - memset(&pkt, 0, sizeof(pkt)); - pkt.raw.buf = (char *) buf; - pkt.raw.len = len; - pkt.eth = (struct eth *) buf; - // mg_hexdump(buf, len > 16 ? 16: len); - if (pkt.raw.len < sizeof(*pkt.eth)) return; // Truncated - runt? - if (ifp->enable_mac_check && - memcmp(pkt.eth->dst, ifp->mac, sizeof(pkt.eth->dst)) != 0 && - memcmp(pkt.eth->dst, broadcast, sizeof(pkt.eth->dst)) != 0) - return; - if (ifp->enable_crc32_check && len > 4) { - len -= 4; // TODO(scaprile): check on bigendian - uint32_t crc = mg_crc32(0, (const char *) buf, len); - if (memcmp((void *) ((size_t) buf + len), &crc, sizeof(crc))) return; - } - if (pkt.eth->type == mg_htons(0x806)) { - pkt.arp = (struct arp *) (pkt.eth + 1); - if (sizeof(*pkt.eth) + sizeof(*pkt.arp) > pkt.raw.len) return; // Truncated - mg_tcpip_call(ifp, MG_TCPIP_EV_ARP, &pkt.raw); - rx_arp(ifp, &pkt); - } else if (pkt.eth->type == mg_htons(0x86dd)) { - pkt.ip6 = (struct ip6 *) (pkt.eth + 1); - if (pkt.raw.len < sizeof(*pkt.eth) + sizeof(*pkt.ip6)) return; // Truncated - if ((pkt.ip6->ver >> 4) != 0x6) return; // Not IP - mkpay(&pkt, pkt.ip6 + 1); - rx_ip6(ifp, &pkt); - } else if (pkt.eth->type == mg_htons(0x800)) { - pkt.ip = (struct ip *) (pkt.eth + 1); - if (pkt.raw.len < sizeof(*pkt.eth) + sizeof(*pkt.ip)) return; // Truncated - // Truncate frame to what IP header tells us - if ((size_t) mg_ntohs(pkt.ip->len) + sizeof(struct eth) < pkt.raw.len) { - pkt.raw.len = (size_t) mg_ntohs(pkt.ip->len) + sizeof(struct eth); +bool mg_ota_begin(size_t new_firmware_size) { + return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_imxrt); +} + +bool mg_ota_write(const void *buf, size_t len) { + return mg_ota_flash_write(buf, len, &s_mg_flash_imxrt); +} + +bool mg_ota_end(void) { + if (mg_ota_flash_end(&s_mg_flash_imxrt)) { + if (0) { // is_dualbank() + // TODO(): no devices so far + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; + } else { + // Swap partitions. Pray power does not go away + MG_INFO(("Swapping partitions, size %u (%u sectors)", + s_mg_flash_imxrt.size, + s_mg_flash_imxrt.size / s_mg_flash_imxrt.secsz)); + MG_INFO(("Do NOT power off...")); + mg_log_level = MG_LL_NONE; + s_flash_irq_disabled = true; + // Runs in RAM, will reset when finished + single_bank_swap( + (char *) s_mg_flash_imxrt.start, + (char *) s_mg_flash_imxrt.start + s_mg_flash_imxrt.size / 2, + s_mg_flash_imxrt.size / 2, s_mg_flash_imxrt.secsz); } - if (pkt.raw.len < sizeof(*pkt.eth) + sizeof(*pkt.ip)) return; // Truncated - if ((pkt.ip->ver >> 4) != 4) return; // Not IP - mkpay(&pkt, pkt.ip + 1); - rx_ip(ifp, &pkt); - } else { - MG_DEBUG(("Unknown eth type %x", mg_htons(pkt.eth->type))); - if (mg_log_level >= MG_LL_VERBOSE) mg_hexdump(buf, len >= 32 ? 32 : len); } + return false; } -static void mg_tcpip_poll(struct mg_tcpip_if *ifp, uint64_t now) { - struct mg_connection *c; - bool expired_1000ms = mg_timer_expired(&ifp->timer_1000ms, 1000, now); - ifp->now = now; +#endif -#if MG_ENABLE_TCPIP_PRINT_DEBUG_STATS - if (expired_1000ms) { - const char *names[] = {"down", "up", "req", "ip", "ready"}; - MG_INFO(("Status: %s, IP: %M, rx:%u, tx:%u, dr:%u, er:%u", - names[ifp->state], mg_print_ip4, &ifp->ip, ifp->nrecv, ifp->nsent, - ifp->ndrop, ifp->nerr)); - } +#ifdef MG_ENABLE_LINES +#line 1 "src/ota_mcxn.c" #endif - // Handle gw ARP request timeout, order is important - if (expired_1000ms && ifp->state == MG_TCPIP_STATE_IP) { - ifp->state = MG_TCPIP_STATE_READY; // keep best-effort MAC - onstatechange(ifp); + + + + +#if MG_OTA == MG_OTA_MCXN + +// - Flash phrase: 16 bytes; smallest portion programmed in one operation. +// - Flash page: 128 bytes; largest portion programmed in one operation. +// - Flash sector: 8 KB; smallest portion that can be erased in one operation. +// - Flash API mg_flash_driver->program: "start" and "len" must be page-size +// aligned; to use 'phrase', FMU register access is needed. Using ROM + +static bool mg_mcxn_write(void *, const void *, size_t); +static bool mg_mcxn_swap(void); + +static struct mg_flash s_mg_flash_mcxn = { + (void *) 0, // Start, filled at init + 0, // Size, filled at init + 0, // Sector size, filled at init + 0, // Align, filled at init + mg_mcxn_write, + mg_mcxn_swap, +}; + +struct mg_flash_config { + uint32_t addr; + uint32_t size; + uint32_t blocks; + uint32_t page_size; + uint32_t sector_size; + uint32_t ffr[6]; + uint32_t reserved0[5]; + uint32_t *bootctx; + bool useahb; +}; + +struct mg_flash_driver_interface { + uint32_t version; + uint32_t (*init)(struct mg_flash_config *); + uint32_t (*erase)(struct mg_flash_config *, uint32_t start, uint32_t len, + uint32_t key); + uint32_t (*program)(struct mg_flash_config *, uint32_t start, uint8_t *src, + uint32_t len); + uint32_t (*verify_erase)(struct mg_flash_config *, uint32_t start, + uint32_t len); + uint32_t (*verify_program)(struct mg_flash_config *, uint32_t start, + uint32_t len, const uint8_t *expected, + uint32_t *addr, uint32_t *failed); + uint32_t reserved1[12]; + uint32_t (*read)(struct mg_flash_config *, uint32_t start, uint8_t *dest, + uint32_t len); + uint32_t reserved2[4]; + uint32_t (*deinit)(struct mg_flash_config *); +}; +#define mg_flash_driver \ + ((struct mg_flash_driver_interface *) (*((uint32_t *) 0x1303fc00 + 4))) +#define MG_MCXN_FLASK_KEY (('k' << 24) | ('e' << 16) | ('f' << 8) | 'l') + +MG_IRAM static bool flash_sector_start(volatile uint32_t *dst) { + char *base = (char *) s_mg_flash_mcxn.start, + *end = base + s_mg_flash_mcxn.size; + volatile char *p = (char *) dst; + return p >= base && p < end && ((p - base) % s_mg_flash_mcxn.secsz) == 0; +} + +MG_IRAM static bool flash_erase(struct mg_flash_config *config, void *addr) { + if (flash_sector_start(addr) == false) { + MG_ERROR(("%p is not on a sector boundary", addr)); + return false; } - // Handle physical interface up/down status - if (expired_1000ms && ifp->driver->up) { - bool up = ifp->driver->up(ifp); - bool current = ifp->state != MG_TCPIP_STATE_DOWN; - if (!up && ifp->enable_dhcp_client) ifp->ip = 0; - if (up != current) { // link state has changed - ifp->state = up == false ? MG_TCPIP_STATE_DOWN - : ifp->enable_dhcp_client || ifp->ip == 0 - ? MG_TCPIP_STATE_UP - : MG_TCPIP_STATE_IP; - onstatechange(ifp); - } else if (!ifp->enable_dhcp_client && ifp->state == MG_TCPIP_STATE_UP && - ifp->ip) { - ifp->state = MG_TCPIP_STATE_IP; // ifp->fn has set an IP - onstatechange(ifp); - } - if (ifp->state == MG_TCPIP_STATE_DOWN) MG_ERROR(("Network is down")); - mg_tcpip_call(ifp, MG_TCPIP_EV_TIMER_1S, NULL); + uint32_t dst = + (uint32_t) addr - (uint32_t) s_mg_flash_mcxn.start; // future-proof + uint32_t status = mg_flash_driver->erase(config, dst, s_mg_flash_mcxn.secsz, + MG_MCXN_FLASK_KEY); + bool ok = (status == 0); + if (!ok) MG_ERROR(("Flash write error: %lu", status)); + MG_DEBUG(("Sector starting at %p erasure: %s", addr, ok ? "ok" : "fail")); + return ok; +} + +#if 0 +// read-while-write, no need to disable IRQs for standalone usage +MG_IRAM static bool mg_mcxn_erase(void *addr) { + uint32_t status; + struct mg_flash_config config; + if ((status = mg_flash_driver->init(&config)) != 0) { + MG_ERROR(("Flash driver init error: %lu", status)); + return false; } - if (ifp->state == MG_TCPIP_STATE_DOWN) return; + bool ok = flash_erase(&config, addr); + mg_flash_driver->deinit(&config); + return ok; +} +#endif + +MG_IRAM static bool mg_mcxn_swap(void) { + // TODO(): no devices so far + return true; +} - // DHCP RFC-2131 (4.4) - if (ifp->enable_dhcp_client && expired_1000ms) { - if (ifp->state == MG_TCPIP_STATE_UP) { - tx_dhcp_discover(ifp); // INIT (4.4.1) - } else if (ifp->state == MG_TCPIP_STATE_READY && - ifp->lease_expire > 0) { // BOUND / RENEWING / REBINDING - if (ifp->now >= ifp->lease_expire) { - ifp->state = MG_TCPIP_STATE_UP, ifp->ip = 0; // expired, release IP - onstatechange(ifp); - } else if (ifp->now + 30UL * 60UL * 1000UL > ifp->lease_expire && - ((ifp->now / 1000) % 60) == 0) { - // hack: 30 min before deadline, try to rebind (4.3.6) every min - tx_dhcp_request_re(ifp, (uint8_t *) broadcast, ifp->ip, 0xffffffff); - } // TODO(): Handle T1 (RENEWING) and T2 (REBINDING) (4.4.5) - } +static bool s_flash_irq_disabled; + +MG_IRAM static bool mg_mcxn_write(void *addr, const void *buf, size_t len) { + bool ok = false; + uint32_t status; + struct mg_flash_config config; + if ((status = mg_flash_driver->init(&config)) != 0) { + MG_ERROR(("Flash driver init error: %lu", status)); + return false; + } + if ((len % s_mg_flash_mcxn.align) != 0) { + MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_mcxn.align)); + goto fwxit; + } + if ((((size_t) addr - (size_t) s_mg_flash_mcxn.start) % + s_mg_flash_mcxn.align) != 0) { + MG_ERROR(("%p is not on a page boundary", addr)); + goto fwxit; } - // Read data from the network - if (ifp->driver->rx != NULL) { // Polling driver. We must call it - size_t len = - ifp->driver->rx(ifp->recv_queue.buf, ifp->recv_queue.size, ifp); - if (len > 0) { - ifp->nrecv++; - mg_tcpip_rx(ifp, ifp->recv_queue.buf, len); + uint32_t *dst = (uint32_t *) addr; + uint32_t *src = (uint32_t *) buf; + uint32_t *end = (uint32_t *) ((char *) buf + len); + ok = true; + + MG_ARM_DISABLE_IRQ(); + while (ok && src < end) { + if (flash_sector_start(dst) && flash_erase(&config, dst) == false) { + ok = false; + break; } - } else { // Interrupt-based driver. Fills recv queue itself - char *buf; - size_t len = mg_queue_next(&ifp->recv_queue, &buf); - if (len > 0) { - mg_tcpip_rx(ifp, buf, len); - mg_queue_del(&ifp->recv_queue, len); + uint32_t dst_ofs = (uint32_t) dst - (uint32_t) s_mg_flash_mcxn.start; + // assume source is in RAM or in a different bank or read-while-write + status = mg_flash_driver->program(&config, dst_ofs, (uint8_t *) src, + s_mg_flash_mcxn.align); + src = (uint32_t *) ((char *) src + s_mg_flash_mcxn.align); + dst = (uint32_t *) ((char *) dst + s_mg_flash_mcxn.align); + if (status != 0) { + MG_ERROR(("Flash write error: %lu", status)); + ok = false; } } + if (!s_flash_irq_disabled) MG_ARM_ENABLE_IRQ(); + MG_DEBUG(("Flash write %lu bytes @ %p: %s.", len, dst, ok ? "ok" : "fail")); - // Process timeouts - for (c = ifp->mgr->conns; c != NULL; c = c->next) { - if ((c->is_udp && !c->is_arplooking) || c->is_listening || c->is_resolving) - continue; - struct connstate *s = (struct connstate *) (c + 1); - uint32_t rem_ip; - memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); - if (now > s->timer) { - if (s->ttype == MIP_TTYPE_ARP) { - mg_error(c, "ARP timeout"); - } else if (c->is_udp) { - continue; - } else if (s->ttype == MIP_TTYPE_ACK && s->acked != s->ack) { - MG_VERBOSE(("%lu ack %x %x", c->id, s->seq, s->ack)); - tx_tcp(ifp, s->mac, rem_ip, TH_ACK, c->loc.port, c->rem.port, - mg_htonl(s->seq), mg_htonl(s->ack), NULL, 0); - s->acked = s->ack; - } else if (s->ttype == MIP_TTYPE_SYN) { - mg_error(c, "Connection timeout"); - } else if (s->ttype == MIP_TTYPE_FIN) { - c->is_closing = 1; - continue; - } else { - if (s->tmiss++ > 2) { - mg_error(c, "keepalive"); - } else { - MG_VERBOSE(("%lu keepalive", c->id)); - tx_tcp(ifp, s->mac, rem_ip, TH_ACK, c->loc.port, c->rem.port, - mg_htonl(s->seq - 1), mg_htonl(s->ack), NULL, 0); - } - } +fwxit: + mg_flash_driver->deinit(&config); + return ok; +} - settmout(c, MIP_TTYPE_KEEPALIVE); +// try to swap (honor dual image), otherwise just overwrite +MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) { + char *tmp = malloc(ss); + // no stdlib calls here + for (size_t ofs = 0; ofs < s; ofs += ss) { + if (tmp != NULL) + for (size_t i = 0; i < ss; i++) tmp[i] = p1[ofs + i]; + mg_mcxn_write(p1 + ofs, p2 + ofs, ss); + if (tmp != NULL) mg_mcxn_write(p2 + ofs, tmp, ss); + } + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; +} + +bool mg_ota_begin(size_t new_firmware_size) { + uint32_t status; + struct mg_flash_config config; + if ((status = mg_flash_driver->init(&config)) != 0) { + MG_ERROR(("Flash driver init error: %lu", status)); + return false; + } + s_mg_flash_mcxn.start = (void *) config.addr; + s_mg_flash_mcxn.size = config.size; + s_mg_flash_mcxn.secsz = config.sector_size; + s_mg_flash_mcxn.align = config.page_size; + mg_flash_driver->deinit(&config); + MG_DEBUG( + ("%lu-byte flash @%p, using %lu-byte sectors with %lu-byte-aligned pages", + s_mg_flash_mcxn.size, s_mg_flash_mcxn.start, s_mg_flash_mcxn.secsz, + s_mg_flash_mcxn.align)); + return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_mcxn); +} + +bool mg_ota_write(const void *buf, size_t len) { + return mg_ota_flash_write(buf, len, &s_mg_flash_mcxn); +} + +bool mg_ota_end(void) { + if (mg_ota_flash_end(&s_mg_flash_mcxn)) { + if (0) { // is_dualbank() + // TODO(): no devices so far + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; + } else { + // Swap partitions. Pray power does not go away + MG_INFO(("Swapping partitions, size %u (%u sectors)", + s_mg_flash_mcxn.size, + s_mg_flash_mcxn.size / s_mg_flash_mcxn.secsz)); + MG_INFO(("Do NOT power off...")); + mg_log_level = MG_LL_NONE; + s_flash_irq_disabled = true; + // Runs in RAM, will reset when finished + single_bank_swap( + (char *) s_mg_flash_mcxn.start, + (char *) s_mg_flash_mcxn.start + s_mg_flash_mcxn.size / 2, + s_mg_flash_mcxn.size / 2, s_mg_flash_mcxn.secsz); } } + return false; } +#endif -// This function executes in interrupt context, thus it should copy data -// somewhere fast. Note that newlib's malloc is not thread safe, thus use -// our lock-free queue with preallocated buffer to copy data and return asap -void mg_tcpip_qwrite(void *buf, size_t len, struct mg_tcpip_if *ifp) { - char *p; - if (mg_queue_book(&ifp->recv_queue, &p, len) >= len) { - memcpy(p, buf, len); - mg_queue_add(&ifp->recv_queue, len); - ifp->nrecv++; - } else { - ifp->ndrop++; +#ifdef MG_ENABLE_LINES +#line 1 "src/ota_picosdk.c" +#endif + + + + +#if MG_OTA == MG_OTA_PICOSDK + +// Both RP2040 and RP2350 have no flash, low-level flash access support in +// bootrom, and high-level support in Pico-SDK (2.0+ for the RP2350) +// - The RP2350 in RISC-V mode is not yet (fully) supported (nor tested) + +static bool mg_picosdk_write(void *, const void *, size_t); +static bool mg_picosdk_swap(void); + +static struct mg_flash s_mg_flash_picosdk = { + (void *) 0x10000000, // Start, not used here; functions handle offset +#ifdef PICO_FLASH_SIZE_BYTES + PICO_FLASH_SIZE_BYTES, // Size, from board definitions +#else + 0x200000, // Size, guess... is 2M enough ? +#endif + FLASH_SECTOR_SIZE, // Sector size, from hardware_flash + FLASH_PAGE_SIZE, // Align, from hardware_flash + mg_picosdk_write, mg_picosdk_swap, +}; + +#define MG_MODULO2(x, m) ((x) & ((m) -1)) + +static bool __no_inline_not_in_flash_func(flash_sector_start)( + volatile uint32_t *dst) { + char *base = (char *) s_mg_flash_picosdk.start, + *end = base + s_mg_flash_picosdk.size; + volatile char *p = (char *) dst; + return p >= base && p < end && + MG_MODULO2(p - base, s_mg_flash_picosdk.secsz) == 0; +} + +static bool __no_inline_not_in_flash_func(flash_erase)(void *addr) { + if (flash_sector_start(addr) == false) { + MG_ERROR(("%p is not on a sector boundary", addr)); + return false; } + void *dst = (void *) ((char *) addr - (char *) s_mg_flash_picosdk.start); + flash_range_erase((uint32_t) dst, s_mg_flash_picosdk.secsz); + MG_DEBUG(("Sector starting at %p erasure", addr)); + return true; } -void mg_tcpip_init(struct mg_mgr *mgr, struct mg_tcpip_if *ifp) { - // If MAC address is not set, make a random one - if (ifp->mac[0] == 0 && ifp->mac[1] == 0 && ifp->mac[2] == 0 && - ifp->mac[3] == 0 && ifp->mac[4] == 0 && ifp->mac[5] == 0) { - ifp->mac[0] = 0x02; // Locally administered, unicast - mg_random(&ifp->mac[1], sizeof(ifp->mac) - 1); - MG_INFO(("MAC not set. Generated random: %M", mg_print_mac, ifp->mac)); +static bool __no_inline_not_in_flash_func(mg_picosdk_swap)(void) { + // TODO(): RP2350 might have some A/B functionality (DS 5.1) + return true; +} + +static bool s_flash_irq_disabled; + +static bool __no_inline_not_in_flash_func(mg_picosdk_write)(void *addr, + const void *buf, + size_t len) { + if ((len % s_mg_flash_picosdk.align) != 0) { + MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_picosdk.align)); + return false; + } + if ((((size_t) addr - (size_t) s_mg_flash_picosdk.start) % + s_mg_flash_picosdk.align) != 0) { + MG_ERROR(("%p is not on a page boundary", addr)); + return false; } - if (ifp->driver->init && !ifp->driver->init(ifp)) { - MG_ERROR(("driver init failed")); - } else { - size_t framesize = 1540; - ifp->tx.buf = (char *) calloc(1, framesize), ifp->tx.len = framesize; - if (ifp->recv_queue.size == 0) - ifp->recv_queue.size = ifp->driver->rx ? framesize : 8192; - ifp->recv_queue.buf = (char *) calloc(1, ifp->recv_queue.size); - ifp->timer_1000ms = mg_millis(); - mgr->priv = ifp; - ifp->mgr = mgr; - ifp->mtu = MG_TCPIP_MTU_DEFAULT; - mgr->extraconnsize = sizeof(struct connstate); - if (ifp->ip == 0) ifp->enable_dhcp_client = true; - memset(ifp->gwmac, 255, sizeof(ifp->gwmac)); // Set best-effort to bcast - mg_random(&ifp->eport, sizeof(ifp->eport)); // Random from 0 to 65535 - ifp->eport |= MG_EPHEMERAL_PORT_BASE; // Random from - // MG_EPHEMERAL_PORT_BASE to 65535 - if (ifp->tx.buf == NULL || ifp->recv_queue.buf == NULL) MG_ERROR(("OOM")); + uint32_t *dst = (uint32_t *) addr; + uint32_t *src = (uint32_t *) buf; + uint32_t *end = (uint32_t *) ((char *) buf + len); + +#ifndef __riscv + MG_ARM_DISABLE_IRQ(); +#else + asm volatile("csrrc zero, mstatus, %0" : : "i"(1 << 3) : "memory"); +#endif + while (src < end) { + uint32_t dst_ofs = (uint32_t) dst - (uint32_t) s_mg_flash_picosdk.start; + if (flash_sector_start(dst) && flash_erase(dst) == false) break; + // flash_range_program() runs in RAM and handles writing up to + // FLASH_PAGE_SIZE bytes. Source must not be in flash + flash_range_program((uint32_t) dst_ofs, (uint8_t *) src, + s_mg_flash_picosdk.align); + src = (uint32_t *) ((char *) src + s_mg_flash_picosdk.align); + dst = (uint32_t *) ((char *) dst + s_mg_flash_picosdk.align); } + if (!s_flash_irq_disabled) { +#ifndef __riscv + MG_ARM_ENABLE_IRQ(); +#else + asm volatile("csrrs mstatus, %0" : : "i"(1 << 3) : "memory"); +#endif + } + MG_DEBUG(("Flash write %lu bytes @ %p.", len, dst)); + return true; } -void mg_tcpip_free(struct mg_tcpip_if *ifp) { - free(ifp->recv_queue.buf); - free(ifp->tx.buf); +// just overwrite instead of swap +static void __no_inline_not_in_flash_func(single_bank_swap)(char *p1, char *p2, + size_t s, + size_t ss) { + char *tmp = malloc(ss); + if (tmp == NULL) return; +#if PICO_RP2040 + uint32_t xip[256 / sizeof(uint32_t)]; + void *dst = (void *) ((char *) p1 - (char *) s_mg_flash_picosdk.start); + size_t count = MG_ROUND_UP(s, ss); + // use SDK function calls to get BootROM function pointers + rom_connect_internal_flash_fn connect = (rom_connect_internal_flash_fn) rom_func_lookup_inline(ROM_FUNC_CONNECT_INTERNAL_FLASH); + rom_flash_exit_xip_fn xit = (rom_flash_exit_xip_fn) rom_func_lookup_inline(ROM_FUNC_FLASH_EXIT_XIP); + rom_flash_range_program_fn program = (rom_flash_range_program_fn) rom_func_lookup_inline(ROM_FUNC_FLASH_RANGE_PROGRAM); + rom_flash_flush_cache_fn flush = (rom_flash_flush_cache_fn) rom_func_lookup_inline(ROM_FUNC_FLASH_FLUSH_CACHE); + // no stdlib calls here. + MG_ARM_DISABLE_IRQ(); + // 2nd bootloader (XIP) is in flash, SDK functions copy it to RAM on entry + for (size_t i = 0; i < 256 / sizeof(uint32_t); i++) + xip[i] = ((uint32_t *) (s_mg_flash_picosdk.start))[i]; + flash_range_erase((uint32_t) dst, count); + // flash has been erased, no XIP to copy. Only BootROM calls possible + for (uint32_t ofs = 0; ofs < s; ofs += ss) { + for (size_t i = 0; i < ss; i++) tmp[i] = p2[ofs + i]; + __compiler_memory_barrier(); + connect(); + xit(); + program((uint32_t) dst + ofs, tmp, ss); + flush(); + ((void (*)(void))((intptr_t) xip + 1))(); // enter XIP again + } + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; // AIRCR = SYSRESETREQ +#else + // RP2350 has bootram and copies second bootloader there, SDK uses that copy, + // It might also be able to take advantage of partition swapping + for (size_t ofs = 0; ofs < s; ofs += ss) { + for (size_t i = 0; i < ss; i++) tmp[i] = p2[ofs + i]; + mg_picosdk_write(p1 + ofs, tmp, ss); + } +#ifndef __riscv + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; // AIRCR = SYSRESETREQ +#else + // TODO(): find a way to do a system reset, like block resets and watchdog +#endif +#endif } -static void send_syn(struct mg_connection *c) { - struct connstate *s = (struct connstate *) (c + 1); - uint32_t isn = mg_htonl((uint32_t) mg_ntohs(c->loc.port)); - struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; - uint32_t rem_ip; - memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); - tx_tcp(ifp, s->mac, rem_ip, TH_SYN, c->loc.port, c->rem.port, isn, 0, NULL, - 0); +bool mg_ota_begin(size_t new_firmware_size) { + return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_picosdk); } -static void mac_resolved(struct mg_connection *c) { - if (c->is_udp) { - c->is_connecting = 0; - mg_call(c, MG_EV_CONNECT, NULL); - } else { - send_syn(c); - settmout(c, MIP_TTYPE_SYN); - } +bool mg_ota_write(const void *buf, size_t len) { + return mg_ota_flash_write(buf, len, &s_mg_flash_picosdk); } -void mg_connect_resolved(struct mg_connection *c) { - struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; - uint32_t rem_ip; - memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); - c->is_resolving = 0; - if (ifp->eport < MG_EPHEMERAL_PORT_BASE) ifp->eport = MG_EPHEMERAL_PORT_BASE; - memcpy(c->loc.ip, &ifp->ip, sizeof(uint32_t)); - c->loc.port = mg_htons(ifp->eport++); - MG_DEBUG(("%lu %M -> %M", c->id, mg_print_ip_port, &c->loc, mg_print_ip_port, - &c->rem)); - mg_call(c, MG_EV_RESOLVE, NULL); - c->is_connecting = 1; - if (c->is_udp && (rem_ip == 0xffffffff || rem_ip == (ifp->ip | ~ifp->mask))) { - struct connstate *s = (struct connstate *) (c + 1); - memset(s->mac, 0xFF, sizeof(s->mac)); // global or local broadcast - mac_resolved(c); - } else if (ifp->ip && ((rem_ip & ifp->mask) == (ifp->ip & ifp->mask)) && - rem_ip != ifp->gw) { // skip if gw (onstatechange -> READY -> ARP) - // If we're in the same LAN, fire an ARP lookup. - MG_DEBUG(("%lu ARP lookup...", c->id)); - mg_tcpip_arp_request(ifp, rem_ip, NULL); - settmout(c, MIP_TTYPE_ARP); - c->is_arplooking = 1; - } else if ((*((uint8_t *) &rem_ip) & 0xE0) == 0xE0) { - struct connstate *s = (struct connstate *) (c + 1); // 224 to 239, E0 to EF - uint8_t mcastp[3] = {0x01, 0x00, 0x5E}; // multicast group - memcpy(s->mac, mcastp, 3); - memcpy(s->mac + 3, ((uint8_t *) &rem_ip) + 1, 3); // 23 LSb - s->mac[3] &= 0x7F; - mac_resolved(c); - } else { - struct connstate *s = (struct connstate *) (c + 1); - memcpy(s->mac, ifp->gwmac, sizeof(ifp->gwmac)); - mac_resolved(c); +bool mg_ota_end(void) { + if (mg_ota_flash_end(&s_mg_flash_picosdk)) { + // Swap partitions. Pray power does not go away + MG_INFO(("Swapping partitions, size %u (%u sectors)", + s_mg_flash_picosdk.size, + s_mg_flash_picosdk.size / s_mg_flash_picosdk.secsz)); + MG_INFO(("Do NOT power off...")); + mg_log_level = MG_LL_NONE; + s_flash_irq_disabled = true; + // Runs in RAM, will reset when finished or return on failure + single_bank_swap( + (char *) s_mg_flash_picosdk.start, + (char *) s_mg_flash_picosdk.start + s_mg_flash_picosdk.size / 2, + s_mg_flash_picosdk.size / 2, s_mg_flash_picosdk.secsz); } + return false; +} +#endif + +#ifdef MG_ENABLE_LINES +#line 1 "src/ota_stm32f.c" +#endif + + + + +#if MG_OTA == MG_OTA_STM32F + +static bool mg_stm32f_write(void *, const void *, size_t); +static bool mg_stm32f_swap(void); + +static struct mg_flash s_mg_flash_stm32f = { + (void *) 0x08000000, // Start + 0, // Size, FLASH_SIZE_REG + 0, // Irregular sector size + 32, // Align, 256 bit + mg_stm32f_write, + mg_stm32f_swap, +}; + +#define MG_FLASH_BASE 0x40023c00 +#define MG_FLASH_KEYR 0x04 +#define MG_FLASH_SR 0x0c +#define MG_FLASH_CR 0x10 +#define MG_FLASH_OPTCR 0x14 +#define MG_FLASH_SIZE_REG_F7 0x1FF0F442 +#define MG_FLASH_SIZE_REG_F4 0x1FFF7A22 + +#define STM_DBGMCU_IDCODE 0xE0042000 +#define STM_DEV_ID (MG_REG(STM_DBGMCU_IDCODE) & (MG_BIT(12) - 1)) +#define SYSCFG_MEMRMP 0x40013800 + +#define MG_FLASH_SIZE_REG_LOCATION \ + ((STM_DEV_ID >= 0x449) ? MG_FLASH_SIZE_REG_F7 : MG_FLASH_SIZE_REG_F4) + +static size_t flash_size(void) { + return (MG_REG(MG_FLASH_SIZE_REG_LOCATION) & 0xFFFF) * 1024; +} + +MG_IRAM static int is_dualbank(void) { + // only F42x/F43x series (0x419) support dual bank + return STM_DEV_ID == 0x419; } -bool mg_open_listener(struct mg_connection *c, const char *url) { - c->loc.port = mg_htons(mg_url_port(url)); - return true; +MG_IRAM static void flash_unlock(void) { + static bool unlocked = false; + if (unlocked == false) { + MG_REG(MG_FLASH_BASE + MG_FLASH_KEYR) = 0x45670123; + MG_REG(MG_FLASH_BASE + MG_FLASH_KEYR) = 0xcdef89ab; + unlocked = true; + } } -static void write_conn(struct mg_connection *c) { - long len = c->is_tls ? mg_tls_send(c, c->send.buf, c->send.len) - : mg_io_send(c, c->send.buf, c->send.len); - if (len == MG_IO_ERR) { - mg_error(c, "tx err"); - } else if (len > 0) { - mg_iobuf_del(&c->send, 0, (size_t) len); - mg_call(c, MG_EV_WRITE, &len); +#define MG_FLASH_CONFIG_16_64_128 1 // used by STM32F7 +#define MG_FLASH_CONFIG_32_128_256 2 // used by STM32F4 and F2 + +MG_IRAM static bool flash_page_start(volatile uint32_t *dst) { + char *base = (char *) s_mg_flash_stm32f.start; + char *end = base + s_mg_flash_stm32f.size; + + if (is_dualbank() && dst >= (uint32_t *) (base + (end - base) / 2)) { + dst = (uint32_t *) ((uint32_t) dst - (end - base) / 2); + } + + uint32_t flash_config = MG_FLASH_CONFIG_16_64_128; + if (STM_DEV_ID >= 0x449) { + flash_config = MG_FLASH_CONFIG_32_128_256; + } + + volatile char *p = (char *) dst; + if (p >= base && p < end) { + if (p < base + 16 * 1024 * 4 * flash_config) { + if ((p - base) % (16 * 1024 * flash_config) == 0) return true; + } else if (p == base + 16 * 1024 * 4 * flash_config) { + return true; + } else if ((p - base) % (128 * 1024 * flash_config) == 0) { + return true; + } } + return false; } -static void init_closure(struct mg_connection *c) { - struct connstate *s = (struct connstate *) (c + 1); - if (c->is_udp == false && c->is_listening == false && - c->is_connecting == false) { // For TCP conns, - struct mg_tcpip_if *ifp = - (struct mg_tcpip_if *) c->mgr->priv; // send TCP FIN - uint32_t rem_ip; - memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); - tx_tcp(ifp, s->mac, rem_ip, TH_FIN | TH_ACK, c->loc.port, c->rem.port, - mg_htonl(s->seq), mg_htonl(s->ack), NULL, 0); - settmout(c, MIP_TTYPE_FIN); +MG_IRAM static int flash_sector(volatile uint32_t *addr) { + char *base = (char *) s_mg_flash_stm32f.start; + char *end = base + s_mg_flash_stm32f.size; + bool addr_in_bank_2 = false; + if (is_dualbank() && addr >= (uint32_t *) (base + (end - base) / 2)) { + addr = (uint32_t *) ((uint32_t) addr - (end - base) / 2); + addr_in_bank_2 = true; + } + volatile char *p = (char *) addr; + uint32_t flash_config = MG_FLASH_CONFIG_16_64_128; + if (STM_DEV_ID >= 0x449) { + flash_config = MG_FLASH_CONFIG_32_128_256; + } + int sector = -1; + if (p >= base && p < end) { + if (p < base + 16 * 1024 * 4 * flash_config) { + sector = (p - base) / (16 * 1024 * flash_config); + } else if (p >= base + 64 * 1024 * flash_config && + p < base + 128 * 1024 * flash_config) { + sector = 4; + } else { + sector = (p - base) / (128 * 1024 * flash_config) + 4; + } } + if (sector == -1) return -1; + if (addr_in_bank_2) sector += 12; // a bank has 12 sectors + return sector; } -static void close_conn(struct mg_connection *c) { - struct connstate *s = (struct connstate *) (c + 1); - mg_iobuf_free(&s->raw); // For TLS connections, release raw data - mg_close_conn(c); +MG_IRAM static bool flash_is_err(void) { + return MG_REG(MG_FLASH_BASE + MG_FLASH_SR) & ((MG_BIT(7) - 1) << 1); } -static bool can_write(struct mg_connection *c) { - return c->is_connecting == 0 && c->is_resolving == 0 && c->send.len > 0 && - c->is_tls_hs == 0 && c->is_arplooking == 0; +MG_IRAM static void flash_wait(void) { + while (MG_REG(MG_FLASH_BASE + MG_FLASH_SR) & (MG_BIT(16))) (void) 0; } -void mg_mgr_poll(struct mg_mgr *mgr, int ms) { - struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) mgr->priv; - struct mg_connection *c, *tmp; - uint64_t now = mg_millis(); - mg_timer_poll(&mgr->timers, now); - if (ifp == NULL || ifp->driver == NULL) return; - mg_tcpip_poll(ifp, now); - for (c = mgr->conns; c != NULL; c = tmp) { - tmp = c->next; - struct connstate *s = (struct connstate *) (c + 1); - mg_call(c, MG_EV_POLL, &now); - MG_VERBOSE(("%lu .. %c%c%c%c%c", c->id, c->is_tls ? 'T' : 't', - c->is_connecting ? 'C' : 'c', c->is_tls_hs ? 'H' : 'h', - c->is_resolving ? 'R' : 'r', c->is_closing ? 'C' : 'c')); - if (c->is_tls && mg_tls_pending(c) > 0) - handle_tls_recv(c, (struct mg_iobuf *) &c->rtls); - if (can_write(c)) write_conn(c); - if (c->is_draining && c->send.len == 0 && s->ttype != MIP_TTYPE_FIN) - init_closure(c); - if (c->is_closing) close_conn(c); - } - (void) ms; +MG_IRAM static void flash_clear_err(void) { + flash_wait(); // Wait until ready + MG_REG(MG_FLASH_BASE + MG_FLASH_SR) = 0xf2; // Clear all errors } -bool mg_send(struct mg_connection *c, const void *buf, size_t len) { - struct mg_tcpip_if *ifp = (struct mg_tcpip_if *) c->mgr->priv; - bool res = false; - uint32_t rem_ip; - memcpy(&rem_ip, c->rem.ip, sizeof(uint32_t)); - if (ifp->ip == 0 || ifp->state != MG_TCPIP_STATE_READY) { - mg_error(c, "net down"); - } else if (c->is_udp && (c->is_arplooking || c->is_resolving)) { - // Fail to send, no target MAC or IP - MG_VERBOSE(("still resolving...")); - } else if (c->is_udp) { - struct connstate *s = (struct connstate *) (c + 1); - len = trim_len(c, len); // Trimming length if necessary - tx_udp(ifp, s->mac, ifp->ip, c->loc.port, rem_ip, c->rem.port, buf, len); - res = true; +MG_IRAM static bool mg_stm32f_erase(void *addr) { + bool ok = false; + if (flash_page_start(addr) == false) { + MG_ERROR(("%p is not on a sector boundary", addr)); } else { - res = mg_iobuf_add(&c->send, c->send.len, buf, len); + int sector = flash_sector(addr); + if (sector < 0) return false; + uint32_t sector_reg = sector; + if (is_dualbank() && sector >= 12) { + // 3.9.8 Flash control register (FLASH_CR) for F42xxx and F43xxx + // BITS[7:3] + sector_reg -= 12; + sector_reg |= MG_BIT(4); + } + flash_unlock(); + flash_wait(); + uint32_t cr = MG_BIT(1); // SER + cr |= MG_BIT(16); // STRT + cr |= (sector_reg & 31) << 3; // sector + MG_REG(MG_FLASH_BASE + MG_FLASH_CR) = cr; + ok = !flash_is_err(); + MG_DEBUG(("Erase sector %lu @ %p %s. CR %#lx SR %#lx", sector, addr, + ok ? "ok" : "fail", MG_REG(MG_FLASH_BASE + MG_FLASH_CR), + MG_REG(MG_FLASH_BASE + MG_FLASH_SR))); + // After we have erased the sector, set CR flags for programming + // 2 << 8 is word write parallelism, bit(0) is PG. RM0385, section 3.7.5 + MG_REG(MG_FLASH_BASE + MG_FLASH_CR) = MG_BIT(0) | (2 << 8); + flash_clear_err(); } - return res; + return ok; } -#endif // MG_ENABLE_TCPIP -#ifdef MG_ENABLE_LINES -#line 1 "src/ota_dummy.c" -#endif +MG_IRAM static bool mg_stm32f_swap(void) { + // STM32 F42x/F43x support dual bank, however, the memory mapping + // change will not be carried through a hard reset. Therefore, we will use + // the single bank approach for this family as well. + return true; +} + +static bool s_flash_irq_disabled; +MG_IRAM static bool mg_stm32f_write(void *addr, + const void *buf, + size_t len) { + if ((len % s_mg_flash_stm32f.align) != 0) { + MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_stm32f.align)); + return false; + } + uint32_t *dst = (uint32_t *) addr; + uint32_t *src = (uint32_t *) buf; + uint32_t *end = (uint32_t *) ((char *) buf + len); + bool ok = true; + MG_ARM_DISABLE_IRQ(); + flash_unlock(); + flash_clear_err(); + MG_REG(MG_FLASH_BASE + MG_FLASH_CR) = MG_BIT(0) | MG_BIT(9); // PG, 32-bit + flash_wait(); + MG_DEBUG(("Writing flash @ %p, %lu bytes", addr, len)); + while (ok && src < end) { + if (flash_page_start(dst) && mg_stm32f_erase(dst) == false) break; + *(volatile uint32_t *) dst++ = *src++; + MG_DSB(); // ensure flash is written with no errors + flash_wait(); + if (flash_is_err()) ok = false; + } + if (!s_flash_irq_disabled) MG_ARM_ENABLE_IRQ(); + MG_DEBUG(("Flash write %lu bytes @ %p: %s. CR %#lx SR %#lx", len, dst, + ok ? "ok" : "fail", MG_REG(MG_FLASH_BASE + MG_FLASH_CR), + MG_REG(MG_FLASH_BASE + MG_FLASH_SR))); + MG_REG(MG_FLASH_BASE + MG_FLASH_CR) &= ~MG_BIT(0); // Clear programming flag + return ok; +} +// just overwrite instead of swap +MG_IRAM void single_bank_swap(char *p1, char *p2, + size_t size) { + // no stdlib calls here + mg_stm32f_write(p1, p2, size); + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; +} -#if MG_OTA == MG_OTA_NONE bool mg_ota_begin(size_t new_firmware_size) { - (void) new_firmware_size; - return true; + s_mg_flash_stm32f.size = flash_size(); + return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32f); } + bool mg_ota_write(const void *buf, size_t len) { - (void) buf, (void) len; - return true; + return mg_ota_flash_write(buf, len, &s_mg_flash_stm32f); } + bool mg_ota_end(void) { - return true; -} -bool mg_ota_commit(void) { - return true; -} -bool mg_ota_rollback(void) { - return true; -} -int mg_ota_status(int fw) { - (void) fw; - return 0; + if (mg_ota_flash_end(&s_mg_flash_stm32f)) { + // Swap partitions. Pray power does not go away + MG_INFO(("Swapping partitions, size %u (%u sectors)", + s_mg_flash_stm32f.size, STM_DEV_ID == 0x449 ? 8 : 12)); + MG_INFO(("Do NOT power off...")); + mg_log_level = MG_LL_NONE; + s_flash_irq_disabled = true; + char *p1 = (char *) s_mg_flash_stm32f.start; + char *p2 = p1 + s_mg_flash_stm32f.size / 2; + size_t size = s_mg_flash_stm32f.size / 2; + // Runs in RAM, will reset when finished + single_bank_swap(p1, p2, size); + } + return false; } -uint32_t mg_ota_crc32(int fw) { - (void) fw; - return 0; +#endif +#ifdef MG_ENABLE_LINES +#line 1 "src/ota_stm32h5.c" +#endif + + + + +#if MG_OTA == MG_OTA_STM32H5 + +static bool mg_stm32h5_write(void *, const void *, size_t); +static bool mg_stm32h5_swap(void); + +static struct mg_flash s_mg_flash_stm32h5 = { + (void *) 0x08000000, // Start + 2 * 1024 * 1024, // Size, 2Mb + 8 * 1024, // Sector size, 8k + 16, // Align, 128 bit + mg_stm32h5_write, + mg_stm32h5_swap, +}; + +#define FLASH_BASE 0x40022000 // Base address of the flash controller +#define FLASH_KEYR (FLASH_BASE + 0x4) // See RM0481 7.11 +#define FLASH_OPTKEYR (FLASH_BASE + 0xc) +#define FLASH_OPTCR (FLASH_BASE + 0x1c) +#define FLASH_NSSR (FLASH_BASE + 0x20) +#define FLASH_NSCR (FLASH_BASE + 0x28) +#define FLASH_NSCCR (FLASH_BASE + 0x30) +#define FLASH_OPTSR_CUR (FLASH_BASE + 0x50) +#define FLASH_OPTSR_PRG (FLASH_BASE + 0x54) + +static void flash_unlock(void) { + static bool unlocked = false; + if (unlocked == false) { + MG_REG(FLASH_KEYR) = 0x45670123; + MG_REG(FLASH_KEYR) = 0Xcdef89ab; + MG_REG(FLASH_OPTKEYR) = 0x08192a3b; + MG_REG(FLASH_OPTKEYR) = 0x4c5d6e7f; + unlocked = true; + } } -uint32_t mg_ota_timestamp(int fw) { - (void) fw; - return 0; + +static int flash_page_start(volatile uint32_t *dst) { + char *base = (char *) s_mg_flash_stm32h5.start, + *end = base + s_mg_flash_stm32h5.size; + volatile char *p = (char *) dst; + return p >= base && p < end && ((p - base) % s_mg_flash_stm32h5.secsz) == 0; } -size_t mg_ota_size(int fw) { - (void) fw; - return 0; + +static bool flash_is_err(void) { + return MG_REG(FLASH_NSSR) & ((MG_BIT(8) - 1) << 17); // RM0481 7.11.9 } -MG_IRAM void mg_ota_boot(void) { + +static void flash_wait(void) { + while ((MG_REG(FLASH_NSSR) & MG_BIT(0)) && + (MG_REG(FLASH_NSSR) & MG_BIT(16)) == 0) { + (void) 0; + } } -#endif -#ifdef MG_ENABLE_LINES -#line 1 "src/ota_esp32.c" -#endif +static void flash_clear_err(void) { + flash_wait(); // Wait until ready + MG_REG(FLASH_NSCCR) = ((MG_BIT(9) - 1) << 16U); // Clear all errors +} +static bool flash_bank_is_swapped(void) { + return MG_REG(FLASH_OPTCR) & MG_BIT(31); // RM0481 7.11.8 +} -#if MG_ARCH == MG_ARCH_ESP32 && MG_OTA == MG_OTA_ESP32 +static bool mg_stm32h5_erase(void *location) { + bool ok = false; + if (flash_page_start(location) == false) { + MG_ERROR(("%p is not on a sector boundary")); + } else { + uintptr_t diff = (char *) location - (char *) s_mg_flash_stm32h5.start; + uint32_t sector = diff / s_mg_flash_stm32h5.secsz; + uint32_t saved_cr = MG_REG(FLASH_NSCR); // Save CR value + flash_unlock(); + flash_clear_err(); + MG_REG(FLASH_NSCR) = 0; + if ((sector < 128 && flash_bank_is_swapped()) || + (sector > 127 && !flash_bank_is_swapped())) { + MG_REG(FLASH_NSCR) |= MG_BIT(31); // Set FLASH_CR_BKSEL + } + if (sector > 127) sector -= 128; + MG_REG(FLASH_NSCR) |= MG_BIT(2) | (sector << 6); // Erase | sector_num + MG_REG(FLASH_NSCR) |= MG_BIT(5); // Start erasing + flash_wait(); + ok = !flash_is_err(); + MG_DEBUG(("Erase sector %lu @ %p: %s. CR %#lx SR %#lx", sector, location, + ok ? "ok" : "fail", MG_REG(FLASH_NSCR), MG_REG(FLASH_NSSR))); + // mg_hexdump(location, 32); + MG_REG(FLASH_NSCR) = saved_cr; // Restore saved CR + } + return ok; +} -static const esp_partition_t *s_ota_update_partition; -static esp_ota_handle_t s_ota_update_handle; -static bool s_ota_success; +static bool mg_stm32h5_swap(void) { + uint32_t desired = flash_bank_is_swapped() ? 0 : MG_BIT(31); + flash_unlock(); + flash_clear_err(); + // printf("OPTSR_PRG 1 %#lx\n", FLASH->OPTSR_PRG); + MG_SET_BITS(MG_REG(FLASH_OPTSR_PRG), MG_BIT(31), desired); + // printf("OPTSR_PRG 2 %#lx\n", FLASH->OPTSR_PRG); + MG_REG(FLASH_OPTCR) |= MG_BIT(1); // OPTSTART + while ((MG_REG(FLASH_OPTSR_CUR) & MG_BIT(31)) != desired) (void) 0; + return true; +} -// Those empty macros do nothing, but mark places in the code which could -// potentially trigger a watchdog reboot due to the log flash erase operation -#define disable_wdt() -#define enable_wdt() +static bool mg_stm32h5_write(void *addr, const void *buf, size_t len) { + if ((len % s_mg_flash_stm32h5.align) != 0) { + MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_stm32h5.align)); + return false; + } + uint32_t *dst = (uint32_t *) addr; + uint32_t *src = (uint32_t *) buf; + uint32_t *end = (uint32_t *) ((char *) buf + len); + bool ok = true; + MG_ARM_DISABLE_IRQ(); + flash_unlock(); + flash_clear_err(); + MG_REG(FLASH_NSCR) = MG_BIT(1); // Set programming flag + while (ok && src < end) { + if (flash_page_start(dst) && mg_stm32h5_erase(dst) == false) { + ok = false; + break; + } + *(volatile uint32_t *) dst++ = *src++; + flash_wait(); + if (flash_is_err()) ok = false; + } + MG_ARM_ENABLE_IRQ(); + MG_DEBUG(("Flash write %lu bytes @ %p: %s. CR %#lx SR %#lx", len, dst, + flash_is_err() ? "fail" : "ok", MG_REG(FLASH_NSCR), + MG_REG(FLASH_NSSR))); + MG_REG(FLASH_NSCR) = 0; // Clear flags + return ok; +} bool mg_ota_begin(size_t new_firmware_size) { - if (s_ota_update_partition != NULL) { - MG_ERROR(("Update in progress. Call mg_ota_end() ?")); - return false; - } else { - s_ota_success = false; - disable_wdt(); - s_ota_update_partition = esp_ota_get_next_update_partition(NULL); - esp_err_t err = esp_ota_begin(s_ota_update_partition, new_firmware_size, - &s_ota_update_handle); - enable_wdt(); - MG_DEBUG(("esp_ota_begin(): %d", err)); - s_ota_success = (err == ESP_OK); - } - return s_ota_success; + return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32h5); } bool mg_ota_write(const void *buf, size_t len) { - disable_wdt(); - esp_err_t err = esp_ota_write(s_ota_update_handle, buf, len); - enable_wdt(); - MG_INFO(("esp_ota_write(): %d", err)); - s_ota_success = err == ESP_OK; - return s_ota_success; + return mg_ota_flash_write(buf, len, &s_mg_flash_stm32h5); } +// Actual bank swap is deferred until reset, it is safe to execute in flash bool mg_ota_end(void) { - esp_err_t err = esp_ota_end(s_ota_update_handle); - MG_DEBUG(("esp_ota_end(%p): %d", s_ota_update_handle, err)); - if (s_ota_success && err == ESP_OK) { - err = esp_ota_set_boot_partition(s_ota_update_partition); - s_ota_success = (err == ESP_OK); - } - MG_DEBUG(("Finished ESP32 OTA, success: %d", s_ota_success)); - s_ota_update_partition = NULL; - return s_ota_success; + if(!mg_ota_flash_end(&s_mg_flash_stm32h5)) return false; + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; + return true; } - #endif #ifdef MG_ENABLE_LINES -#line 1 "src/ota_flash.c" +#line 1 "src/ota_stm32h7.c" #endif +#if MG_OTA == MG_OTA_STM32H7 || MG_OTA == MG_OTA_STM32H7_DUAL_CORE -// This OTA implementation uses the internal flash API outlined in device.h -// It splits flash into 2 equal partitions, and stores OTA status in the -// last sector of the partition. +// - H723/735 RM 4.3.3: Note: The application can simultaneously request a read +// and a write operation through the AXI interface. +// - We only need IRAM for partition swapping in the H723, however, all +// related functions must reside in IRAM for this to be possible. +// - Linker files for other devices won't define a .iram section so there's no +// associated penalty -#if MG_OTA == MG_OTA_FLASH +static bool mg_stm32h7_write(void *, const void *, size_t); +static bool mg_stm32h7_swap(void); -#define MG_OTADATA_KEY 0xb07afed0 +static struct mg_flash s_mg_flash_stm32h7 = { + (void *) 0x08000000, // Start + 0, // Size, FLASH_SIZE_REG + 128 * 1024, // Sector size, 128k + 32, // Align, 256 bit + mg_stm32h7_write, + mg_stm32h7_swap, +}; -static char *s_addr; // Current address to write to -static size_t s_size; // Firmware size to flash. In-progress indicator -static uint32_t s_crc32; // Firmware checksum +#define FLASH_BASE1 0x52002000 // Base address for bank1 +#define FLASH_BASE2 0x52002100 // Base address for bank2 +#define FLASH_KEYR 0x04 // See RM0433 4.9.2 +#define FLASH_OPTKEYR 0x08 +#define FLASH_OPTCR 0x18 +#define FLASH_SR 0x10 +#define FLASH_CR 0x0c +#define FLASH_CCR 0x14 +#define FLASH_OPTSR_CUR 0x1c +#define FLASH_OPTSR_PRG 0x20 +#define FLASH_SIZE_REG 0x1ff1e880 -struct mg_otadata { - uint32_t crc32, size, timestamp, status; -}; +#define IS_DUALCORE() (MG_OTA == MG_OTA_STM32H7_DUAL_CORE) -bool mg_ota_begin(size_t new_firmware_size) { - bool ok = false; - if (s_size) { - MG_ERROR(("OTA already in progress. Call mg_ota_end()")); - } else { - size_t half = mg_flash_size() / 2, max = half - mg_flash_sector_size(); - s_crc32 = 0; - s_addr = (char *) mg_flash_start() + half; - MG_DEBUG(("Firmware %lu bytes, max %lu", new_firmware_size, max)); - if (new_firmware_size < max) { - ok = true; - s_size = new_firmware_size; - MG_INFO(("Starting OTA, firmware size %lu", s_size)); - } else { - MG_ERROR(("Firmware %lu is too big to fit %lu", new_firmware_size, max)); - } +MG_IRAM static bool is_dualbank(void) { + if (IS_DUALCORE()) { + // H745/H755 and H747/H757 are running on dual core. + // Using only the 1st bank (mapped to CM7), in order not to interfere + // with the 2nd bank (CM4), possibly causing CM4 to boot unexpectedly. + return false; } - return ok; + return (s_mg_flash_stm32h7.size < 2 * 1024 * 1024) ? false : true; } -bool mg_ota_write(const void *buf, size_t len) { - bool ok = false; - if (s_size == 0) { - MG_ERROR(("OTA is not started, call mg_ota_begin()")); - } else { - size_t align = mg_flash_write_align(); - size_t len_aligned_down = MG_ROUND_DOWN(len, align); - if (len_aligned_down) ok = mg_flash_write(s_addr, buf, len_aligned_down); - if (len_aligned_down < len) { - size_t left = len - len_aligned_down; - char tmp[align]; - memset(tmp, 0xff, sizeof(tmp)); - memcpy(tmp, (char *) buf + len_aligned_down, left); - ok = mg_flash_write(s_addr + len_aligned_down, tmp, sizeof(tmp)); +MG_IRAM static void flash_unlock(void) { + static bool unlocked = false; + if (unlocked == false) { + MG_REG(FLASH_BASE1 + FLASH_KEYR) = 0x45670123; + MG_REG(FLASH_BASE1 + FLASH_KEYR) = 0xcdef89ab; + if (is_dualbank()) { + MG_REG(FLASH_BASE2 + FLASH_KEYR) = 0x45670123; + MG_REG(FLASH_BASE2 + FLASH_KEYR) = 0xcdef89ab; } - s_crc32 = mg_crc32(s_crc32, (char *) buf, len); // Update CRC - MG_DEBUG(("%#x %p %lu -> %d", s_addr - len, buf, len, ok)); - s_addr += len; + MG_REG(FLASH_BASE1 + FLASH_OPTKEYR) = 0x08192a3b; // opt reg is "shared" + MG_REG(FLASH_BASE1 + FLASH_OPTKEYR) = 0x4c5d6e7f; // thus unlock once + unlocked = true; } - return ok; } -MG_IRAM static uint32_t mg_fwkey(int fw) { - uint32_t key = MG_OTADATA_KEY + fw; - int bank = mg_flash_bank(); - if (bank == 2 && fw == MG_FIRMWARE_PREVIOUS) key--; - if (bank == 2 && fw == MG_FIRMWARE_CURRENT) key++; - return key; +MG_IRAM static bool flash_page_start(volatile uint32_t *dst) { + char *base = (char *) s_mg_flash_stm32h7.start, + *end = base + s_mg_flash_stm32h7.size; + volatile char *p = (char *) dst; + return p >= base && p < end && ((p - base) % s_mg_flash_stm32h7.secsz) == 0; } -bool mg_ota_end(void) { - char *base = (char *) mg_flash_start() + mg_flash_size() / 2; - bool ok = false; - if (s_size) { - size_t size = s_addr - base; - uint32_t crc32 = mg_crc32(0, base, s_size); - if (size == s_size && crc32 == s_crc32) { - uint32_t now = (uint32_t) (mg_now() / 1000); - struct mg_otadata od = {crc32, size, now, MG_OTA_FIRST_BOOT}; - uint32_t key = mg_fwkey(MG_FIRMWARE_PREVIOUS); - ok = mg_flash_save(NULL, key, &od, sizeof(od)); - } - MG_DEBUG(("CRC: %x/%x, size: %lu/%lu, status: %s", s_crc32, crc32, s_size, - size, ok ? "ok" : "fail")); - s_size = 0; - if (ok) ok = mg_flash_swap_bank(); - } - MG_INFO(("Finishing OTA: %s", ok ? "ok" : "fail")); - return ok; +MG_IRAM static bool flash_is_err(uint32_t bank) { + return MG_REG(bank + FLASH_SR) & ((MG_BIT(11) - 1) << 17); // RM0433 4.9.5 } -MG_IRAM static struct mg_otadata mg_otadata(int fw) { - uint32_t key = mg_fwkey(fw); - struct mg_otadata od = {}; - MG_INFO(("Loading %s OTA data", fw == MG_FIRMWARE_CURRENT ? "curr" : "prev")); - mg_flash_load(NULL, key, &od, sizeof(od)); - // MG_DEBUG(("Loaded OTA data. fw %d, bank %d, key %p", fw, bank, key)); - // mg_hexdump(&od, sizeof(od)); - return od; +MG_IRAM static void flash_wait(uint32_t bank) { + while (MG_REG(bank + FLASH_SR) & (MG_BIT(0) | MG_BIT(2))) (void) 0; } -int mg_ota_status(int fw) { - struct mg_otadata od = mg_otadata(fw); - return od.status; -} -uint32_t mg_ota_crc32(int fw) { - struct mg_otadata od = mg_otadata(fw); - return od.crc32; +MG_IRAM static void flash_clear_err(uint32_t bank) { + flash_wait(bank); // Wait until ready + MG_REG(bank + FLASH_CCR) = ((MG_BIT(11) - 1) << 16U); // Clear all errors } -uint32_t mg_ota_timestamp(int fw) { - struct mg_otadata od = mg_otadata(fw); - return od.timestamp; + +MG_IRAM static bool flash_bank_is_swapped(uint32_t bank) { + return MG_REG(bank + FLASH_OPTCR) & MG_BIT(31); // RM0433 4.9.7 } -size_t mg_ota_size(int fw) { - struct mg_otadata od = mg_otadata(fw); - return od.size; + +// Figure out flash bank based on the address +MG_IRAM static uint32_t flash_bank(void *addr) { + size_t ofs = (char *) addr - (char *) s_mg_flash_stm32h7.start; + if (!is_dualbank()) return FLASH_BASE1; + return ofs < s_mg_flash_stm32h7.size / 2 ? FLASH_BASE1 : FLASH_BASE2; } -MG_IRAM bool mg_ota_commit(void) { - bool ok = true; - struct mg_otadata od = mg_otadata(MG_FIRMWARE_CURRENT); - if (od.status != MG_OTA_COMMITTED) { - od.status = MG_OTA_COMMITTED; - MG_INFO(("Committing current firmware, OD size %lu", sizeof(od))); - ok = mg_flash_save(NULL, mg_fwkey(MG_FIRMWARE_CURRENT), &od, sizeof(od)); +// read-while-write, no need to disable IRQs for standalone usage +MG_IRAM static bool mg_stm32h7_erase(void *addr) { + bool ok = false; + if (flash_page_start(addr) == false) { + MG_ERROR(("%p is not on a sector boundary", addr)); + } else { + uintptr_t diff = (char *) addr - (char *) s_mg_flash_stm32h7.start; + uint32_t sector = diff / s_mg_flash_stm32h7.secsz; + uint32_t bank = flash_bank(addr); + uint32_t saved_cr = MG_REG(bank + FLASH_CR); // Save CR value + + flash_unlock(); + if (sector > 7) sector -= 8; + + flash_clear_err(bank); + MG_REG(bank + FLASH_CR) = MG_BIT(5); // 32-bit write parallelism + MG_REG(bank + FLASH_CR) |= (sector & 7U) << 8U; // Sector to erase + MG_REG(bank + FLASH_CR) |= MG_BIT(2); // Sector erase bit + MG_REG(bank + FLASH_CR) |= MG_BIT(7); // Start erasing + ok = !flash_is_err(bank); + MG_DEBUG(("Erase sector %lu @ %p %s. CR %#lx SR %#lx", sector, addr, + ok ? "ok" : "fail", MG_REG(bank + FLASH_CR), + MG_REG(bank + FLASH_SR))); + MG_REG(bank + FLASH_CR) = saved_cr; // Restore CR } return ok; } -bool mg_ota_rollback(void) { - MG_DEBUG(("Rolling firmware back")); - if (mg_flash_bank() == 0) { - // No dual bank support. Mark previous firmware as FIRST_BOOT - struct mg_otadata prev = mg_otadata(MG_FIRMWARE_PREVIOUS); - prev.status = MG_OTA_FIRST_BOOT; - return mg_flash_save(NULL, MG_OTADATA_KEY + MG_FIRMWARE_PREVIOUS, &prev, - sizeof(prev)); - } else { - return mg_flash_swap_bank(); - } +MG_IRAM static bool mg_stm32h7_swap(void) { + if (!is_dualbank()) return true; + uint32_t bank = FLASH_BASE1; + uint32_t desired = flash_bank_is_swapped(bank) ? 0 : MG_BIT(31); + flash_unlock(); + flash_clear_err(bank); + // printf("OPTSR_PRG 1 %#lx\n", FLASH->OPTSR_PRG); + MG_SET_BITS(MG_REG(bank + FLASH_OPTSR_PRG), MG_BIT(31), desired); + // printf("OPTSR_PRG 2 %#lx\n", FLASH->OPTSR_PRG); + MG_REG(bank + FLASH_OPTCR) |= MG_BIT(1); // OPTSTART + while ((MG_REG(bank + FLASH_OPTSR_CUR) & MG_BIT(31)) != desired) (void) 0; + return true; } -MG_IRAM void mg_ota_boot(void) { - MG_INFO(("Booting. Flash bank: %d", mg_flash_bank())); - struct mg_otadata curr = mg_otadata(MG_FIRMWARE_CURRENT); - struct mg_otadata prev = mg_otadata(MG_FIRMWARE_PREVIOUS); +static bool s_flash_irq_disabled; - if (curr.status == MG_OTA_FIRST_BOOT) { - if (prev.status == MG_OTA_UNAVAILABLE) { - MG_INFO(("Setting previous firmware state to committed")); - prev.status = MG_OTA_COMMITTED; - mg_flash_save(NULL, mg_fwkey(MG_FIRMWARE_PREVIOUS), &prev, sizeof(prev)); - } - curr.status = MG_OTA_UNCOMMITTED; - MG_INFO(("First boot, setting status to UNCOMMITTED")); - mg_flash_save(NULL, mg_fwkey(MG_FIRMWARE_CURRENT), &curr, sizeof(curr)); - } else if (prev.status == MG_OTA_FIRST_BOOT && mg_flash_bank() == 0) { - // Swap paritions. Pray power does not disappear - size_t fs = mg_flash_size(), ss = mg_flash_sector_size(); - char *partition1 = mg_flash_start(); - char *partition2 = mg_flash_start() + fs / 2; - size_t ofs, max = fs / 2 - ss; // Set swap size to the whole partition - - if (curr.status != MG_OTA_UNAVAILABLE && - prev.status != MG_OTA_UNAVAILABLE) { - // We know exact sizes of both firmwares. - // Shrink swap size to the MAX(firmware1, firmware2) - size_t sz = curr.size > prev.size ? curr.size : prev.size; - if (sz > 0 && sz < max) max = sz; +MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) { + if ((len % s_mg_flash_stm32h7.align) != 0) { + MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_stm32h7.align)); + return false; + } + uint32_t bank = flash_bank(addr); + uint32_t *dst = (uint32_t *) addr; + uint32_t *src = (uint32_t *) buf; + uint32_t *end = (uint32_t *) ((char *) buf + len); + bool ok = true; + MG_ARM_DISABLE_IRQ(); + flash_unlock(); + flash_clear_err(bank); + MG_REG(bank + FLASH_CR) = MG_BIT(1); // Set programming flag + MG_REG(bank + FLASH_CR) |= MG_BIT(5); // 32-bit write parallelism + while (ok && src < end) { + if (flash_page_start(dst) && mg_stm32h7_erase(dst) == false) { + ok = false; + break; } + *(volatile uint32_t *) dst++ = *src++; + flash_wait(bank); + if (flash_is_err(bank)) ok = false; + } + if (!s_flash_irq_disabled) MG_ARM_ENABLE_IRQ(); + MG_DEBUG(("Flash write %lu bytes @ %p: %s. CR %#lx SR %#lx", len, dst, + ok ? "ok" : "fail", MG_REG(bank + FLASH_CR), + MG_REG(bank + FLASH_SR))); + MG_REG(bank + FLASH_CR) &= ~MG_BIT(1); // Clear programming flag + return ok; +} + +// just overwrite instead of swap +MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) { + // no stdlib calls here + for (size_t ofs = 0; ofs < s; ofs += ss) { + mg_stm32h7_write(p1 + ofs, p2 + ofs, ss); + } + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; +} - // MG_OTA_FIRST_BOOT -> MG_OTA_UNCOMMITTED - prev.status = MG_OTA_UNCOMMITTED; - mg_flash_save(NULL, MG_OTADATA_KEY + MG_FIRMWARE_CURRENT, &prev, - sizeof(prev)); - mg_flash_save(NULL, MG_OTADATA_KEY + MG_FIRMWARE_PREVIOUS, &curr, - sizeof(curr)); +bool mg_ota_begin(size_t new_firmware_size) { + s_mg_flash_stm32h7.size = MG_REG(FLASH_SIZE_REG) * 1024; + if (IS_DUALCORE()) { + // Using only the 1st bank (mapped to CM7) + s_mg_flash_stm32h7.size /= 2; + } + return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32h7); +} - MG_INFO(("Swapping partitions, size %u (%u sectors)", max, max / ss)); - MG_INFO(("Do NOT power off...")); - mg_log_level = MG_LL_NONE; +bool mg_ota_write(const void *buf, size_t len) { + return mg_ota_flash_write(buf, len, &s_mg_flash_stm32h7); +} - // We use the last sector of partition2 for OTA data/config storage - // Therefore we can use last sector of partition1 for swapping - char *tmpsector = partition1 + fs / 2 - ss; // Last sector of partition1 - (void) tmpsector; - for (ofs = 0; ofs < max; ofs += ss) { - // mg_flash_erase(tmpsector); - mg_flash_write(tmpsector, partition1 + ofs, ss); - // mg_flash_erase(partition1 + ofs); - mg_flash_write(partition1 + ofs, partition2 + ofs, ss); - // mg_flash_erase(partition2 + ofs); - mg_flash_write(partition2 + ofs, tmpsector, ss); +bool mg_ota_end(void) { + if (mg_ota_flash_end(&s_mg_flash_stm32h7)) { + if (is_dualbank()) { + // Bank swap is deferred until reset, been executing in flash, reset + *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; + } else { + // Swap partitions. Pray power does not go away + MG_INFO(("Swapping partitions, size %u (%u sectors)", + s_mg_flash_stm32h7.size, + s_mg_flash_stm32h7.size / s_mg_flash_stm32h7.secsz)); + MG_INFO(("Do NOT power off...")); + mg_log_level = MG_LL_NONE; + s_flash_irq_disabled = true; + // Runs in RAM, will reset when finished + single_bank_swap( + (char *) s_mg_flash_stm32h7.start, + (char *) s_mg_flash_stm32h7.start + s_mg_flash_stm32h7.size / 2, + s_mg_flash_stm32h7.size / 2, s_mg_flash_stm32h7.secsz); } - mg_device_reset(); } + return false; } #endif @@ -16391,6 +16844,9 @@ bool mg_random(void *buf, size_t len) { #if MG_ARCH == MG_ARCH_ESP32 while (len--) *p++ = (unsigned char) (esp_random() & 255); success = true; +#elif MG_ARCH == MG_ARCH_PICOSDK + while (len--) *p++ = (unsigned char) (get_rand_32() & 255); + success = true; #elif MG_ARCH == MG_ARCH_WIN32 static bool initialised = false; #if defined(_MSC_VER) && _MSC_VER < 1700 @@ -16405,15 +16861,16 @@ bool mg_random(void *buf, size_t len) { success = CryptGenRandom(hProv, len, p); } #else - // BCrypt is a "new generation" strong crypto API, so try it first - static BCRYPT_ALG_HANDLE hProv; - if (initialised == false && - BCryptOpenAlgorithmProvider(&hProv, BCRYPT_RNG_ALGORITHM, NULL, 0) == 0) { - initialised = true; - } - if (initialised == true) { - success = BCryptGenRandom(hProv, p, (ULONG) len, 0) == 0; + size_t i; + for (i = 0; i < len; i++) { + unsigned int rand_v; + if (rand_s(&rand_v) == 0) { + p[i] = (unsigned char)(rand_v & 255); + } else { + break; + } } + success = (i == len); #endif #elif MG_ARCH == MG_ARCH_UNIX @@ -16526,7 +16983,7 @@ bool mg_path_is_sane(const struct mg_str path) { uint64_t mg_millis(void) { #if MG_ARCH == MG_ARCH_WIN32 return GetTickCount(); -#elif MG_ARCH == MG_ARCH_RP2040 +#elif MG_ARCH == MG_ARCH_PICOSDK return time_us_64() / 1000; #elif MG_ARCH == MG_ARCH_ESP8266 || MG_ARCH == MG_ARCH_ESP32 || \ MG_ARCH == MG_ARCH_FREERTOS @@ -18526,6 +18983,243 @@ struct mg_tcpip_driver mg_tcpip_driver_tm4c = {mg_tcpip_driver_tm4c_init, mg_tcpip_driver_tm4c_up}; #endif +#ifdef MG_ENABLE_LINES +#line 1 "src/drivers/tms570.c" +#endif + + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_TMS570) && MG_ENABLE_DRIVER_TMS570 +struct tms570_emac_ctrl { + volatile uint32_t REVID, SOFTRESET, RESERVED1[1], INTCONTROL, C0RXTHRESHEN, + C0RXEN, C0TXEN, C0MISCEN, RESERVED2[8], + C0RXTHRESHSTAT, C0RXSTAT, C0TXSTAT, C0MISCSTAT, + RESERVED3[8], + C0RXIMAX, C0TXIMAX; +}; +struct tms570_emac { + volatile uint32_t TXREVID, TXCONTROL, TXTEARDOWN, RESERVED1[1], RXREVID, + RXCONTROL, RXTEARDOWN, RESERVED2[25], TXINTSTATRAW,TXINTSTATMASKED, + TXINTMASKSET, TXINTMASKCLEAR, MACINVECTOR, MACEOIVECTOR, RESERVED8[2], RXINTSTATRAW, + RXINTSTATMASKED, RXINTMASKSET, RXINTMASKCLEAR, MACINTSTATRAW, MACINTSTATMASKED, + MACINTMASKSET, MACINTMASKCLEAR, RESERVED3[16], RXMBPENABLE, RXUNICASTSET, + RXUNICASTCLEAR, RXMAXLEN, RXBUFFEROFFSET, RXFILTERLOWTHRESH, RESERVED9[2], RXFLOWTHRESH[8], + RXFREEBUFFER[8], MACCONTROL, MACSTATUS, EMCONTROL, FIFOCONTROL, MACCONFIG, + SOFTRESET, RESERVED4[22], MACSRCADDRLO, MACSRCADDRHI, MACHASH1, MACHASH2, + BOFFTEST, TPACETEST, RXPAUSE, TXPAUSE, RESERVED5[4], RXGOODFRAMES, RXBCASTFRAMES, + RXMCASTFRAMES, RXPAUSEFRAMES, RXCRCERRORS, RXALIGNCODEERRORS, RXOVERSIZED, + RXJABBER, RXUNDERSIZED, RXFRAGMENTS, RXFILTERED, RXQOSFILTERED, RXOCTETS, + TXGOODFRAMES, TXBCASTFRAMES, TXMCASTFRAMES, TXPAUSEFRAMES, TXDEFERRED, + TXCOLLISION, TXSINGLECOLL, TXMULTICOLL, TXEXCESSIVECOLL, TXLATECOLL, + TXUNDERRUN, TXCARRIERSENSE, TXOCTETS, FRAME64, FRAME65T127, FRAME128T255, + FRAME256T511, FRAME512T1023, FRAME1024TUP, NETOCTETS, RXSOFOVERRUNS, + RXMOFOVERRUNS, RXDMAOVERRUNS, RESERVED6[156], MACADDRLO, MACADDRHI, + MACINDEX, RESERVED7[61], TXHDP[8], RXHDP[8], TXCP[8], RXCP[8]; +}; +struct tms570_mdio { + volatile uint32_t REVID, CONTROL, ALIVE, LINK, LINKINTRAW, LINKINTMASKED, + RESERVED1[2], USERINTRAW, USERINTMASKED, USERINTMASKSET, USERINTMASKCLEAR, + RESERVED2[20], USERACCESS0, USERPHYSEL0, USERACCESS1, USERPHYSEL1; +}; +#define SWAP32(x) ( (((x) & 0x000000FF) << 24) | \ + (((x) & 0x0000FF00) << 8) | \ + (((x) & 0x00FF0000) >> 8) | \ + (((x) & 0xFF000000) >> 24) ) +#undef EMAC +#undef EMAC_CTRL +#undef MDIO +#define EMAC ((struct tms570_emac *) (uintptr_t) 0xFCF78000) +#define EMAC_CTRL ((struct tms570_emac_ctrl *) (uintptr_t) 0xFCF78800) +#define MDIO ((struct tms570_mdio *) (uintptr_t) 0xFCF78900) +#define ETH_PKT_SIZE 1540 // Max frame size +#define ETH_DESC_CNT 4 // Descriptors count +#define ETH_DS 4 // Descriptor size (words) +static uint32_t s_txdesc[ETH_DESC_CNT][ETH_DS] + __attribute__((section(".ETH_CPPI"), aligned(4))); // TX descriptors +static uint32_t s_rxdesc[ETH_DESC_CNT][ETH_DS] + __attribute__((section(".ETH_CPPI"), aligned(4))); // RX descriptors +static uint8_t s_rxbuf[ETH_DESC_CNT][ETH_PKT_SIZE] + __attribute__((aligned(4))); // RX ethernet buffers +static uint8_t s_txbuf[ETH_DESC_CNT][ETH_PKT_SIZE] + __attribute__((aligned(4))); // TX ethernet buffers +static struct mg_tcpip_if *s_ifp; // MIP interface +static uint16_t emac_read_phy(uint8_t addr, uint8_t reg) { + while(MDIO->USERACCESS0 & MG_BIT(31)) (void) 0; + MDIO->USERACCESS0 = MG_BIT(31) | ((reg & 0x1f) << 21) | + ((addr & 0x1f) << 16); + while(MDIO->USERACCESS0 & MG_BIT(31)) (void) 0; + return MDIO->USERACCESS0 & 0xffff; +} +static void emac_write_phy(uint8_t addr, uint8_t reg, uint16_t val) { + while(MDIO->USERACCESS0 & MG_BIT(31)) (void) 0; + MDIO->USERACCESS0 = MG_BIT(31) | MG_BIT(30) | ((reg & 0x1f) << 21) | + ((addr & 0x1f) << 16) | (val & 0xffff); + while(MDIO->USERACCESS0 & MG_BIT(31)) (void) 0; +} +static bool mg_tcpip_driver_tms570_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_tms570_data *d = + (struct mg_tcpip_driver_tms570_data *) ifp->driver_data; + s_ifp = ifp; + EMAC_CTRL->SOFTRESET = MG_BIT(0); // Reset the EMAC Control Module + while(EMAC_CTRL->SOFTRESET & MG_BIT(0)) (void) 0; // wait + EMAC->SOFTRESET = MG_BIT(0); // Reset the EMAC Module + while(EMAC->SOFTRESET & MG_BIT(0)) (void) 0; + EMAC->MACCONTROL = 0; + EMAC->RXCONTROL = 0; + EMAC->TXCONTROL = 0; + // Initialize all the header descriptor pointer registers + uint32_t i; + for(i = 0; i < ETH_DESC_CNT; i++) { + EMAC->RXHDP[i] = 0; + EMAC->TXHDP[i] = 0; + EMAC->RXCP[i] = 0; + EMAC->TXCP[i] = 0; + ///EMAC->RXFREEBUFFER[i] = 0xff; + } + // Clear the interrupt enable for all the channels + EMAC->TXINTMASKCLEAR = 0xff; + EMAC->RXINTMASKCLEAR = 0xff; + EMAC->MACHASH1 = 0; + EMAC->MACHASH2 = 0; + EMAC->RXBUFFEROFFSET = 0; + EMAC->RXUNICASTCLEAR = 0xff; + EMAC->RXUNICASTSET = 0; + EMAC->RXMBPENABLE = 0; + // init MDIO + // MDIO_CLK frequency = VCLK3/(CLKDIV + 1). (MDIO must be between 1.0 - 2.5Mhz) + uint32_t clkdiv = 75; // VCLK is configured to 75Mhz + // CLKDIV, ENABLE, PREAMBLE, FAULTENB + MDIO->CONTROL = (clkdiv - 1) | MG_BIT(30) | MG_BIT(20) | MG_BIT(18); + volatile int delay = 0xfff; + while (delay-- != 0) (void) 0; + struct mg_phy phy = {emac_read_phy, emac_write_phy}; + mg_phy_init(&phy, d->phy_addr, MG_PHY_CLOCKS_MAC); + // set the mac address + EMAC->MACSRCADDRHI = ifp->mac[0] | (ifp->mac[1] << 8) | (ifp->mac[2] << 16) | + (ifp->mac[3] << 24); + EMAC->MACSRCADDRLO = ifp->mac[4] | (ifp->mac[5] << 8); + uint32_t channel; + for (channel = 0; channel < 8; channel++) { + EMAC->MACINDEX = channel; + EMAC->MACADDRHI = ifp->mac[0] | (ifp->mac[1] << 8) | (ifp->mac[2] << 16) | + (ifp->mac[3] << 24); + EMAC->MACADDRLO = ifp->mac[4] | (ifp->mac[5] << 8) | MG_BIT(20) | + MG_BIT(19) | (channel << 16); + } + EMAC->RXUNICASTSET = 1; // accept unicast frames; + EMAC->RXMBPENABLE = MG_BIT(30) | MG_BIT(13); // CRC, broadcast; + + // Initialize the descriptors + for (i = 0; i < ETH_DESC_CNT; i++) { + if (i < ETH_DESC_CNT - 1) { + s_txdesc[i][0] = 0; + s_rxdesc[i][0] = SWAP32(((uint32_t) &s_rxdesc[i + 1][0])); + } + s_txdesc[i][1] = SWAP32(((uint32_t) s_txbuf[i])); + s_rxdesc[i][1] = SWAP32(((uint32_t) s_rxbuf[i])); + s_txdesc[i][2] = 0; + s_rxdesc[i][2] = SWAP32(ETH_PKT_SIZE); + s_txdesc[i][3] = 0; + s_rxdesc[i][3] = SWAP32(MG_BIT(29)); // OWN + } + s_txdesc[ETH_DESC_CNT - 1][0] = 0; + s_rxdesc[ETH_DESC_CNT - 1][0] = 0; + + EMAC->MACCONTROL = MG_BIT(5) | MG_BIT(0); // Enable MII, Full-duplex + //EMAC->TXINTMASKSET = 1; // Enable TX interrupt + EMAC->RXINTMASKSET = 1; // Enable RX interrupt + //EMAC_CTRL->C0TXEN = 1; // TX completion interrupt + EMAC_CTRL->C0RXEN = 1; // RX completion interrupt + EMAC->TXCONTROL = 1; // TXEN + EMAC->RXCONTROL = 1; // RXEN + EMAC->RXHDP[0] = (uint32_t) &s_rxdesc[0][0]; + return true; +} +static uint32_t s_txno; +static size_t mg_tcpip_driver_tms570_tx(const void *buf, size_t len, + struct mg_tcpip_if *ifp) { + if (len > sizeof(s_txbuf[s_txno])) { + MG_ERROR(("Frame too big, %ld", (long) len)); + len = 0; // fail + } else if ((s_txdesc[s_txno][3] & SWAP32(MG_BIT(29)))) { + ifp->nerr++; + MG_ERROR(("No descriptors available")); + len = 0; // fail + } else { + memcpy(s_txbuf[s_txno], buf, len); // Copy data + if (len < 128) len = 128; + s_txdesc[s_txno][2] = SWAP32((uint32_t) len); // Set data len + s_txdesc[s_txno][3] = + SWAP32(MG_BIT(31) | MG_BIT(30) | MG_BIT(29) | len); // SOP, EOP, OWN, length + + while(EMAC->TXHDP[0] != 0) (void) 0; + EMAC->TXHDP[0] = (uint32_t) &s_txdesc[s_txno][0]; + if(++s_txno == ETH_DESC_CNT) { + s_txno = 0; + } + } + return len; + (void) ifp; +} +static bool mg_tcpip_driver_tms570_up(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_tms570_data *d = + (struct mg_tcpip_driver_tms570_data *) ifp->driver_data; + uint8_t speed = MG_PHY_SPEED_10M; + bool up = false, full_duplex = false; + struct mg_phy phy = {emac_read_phy, emac_write_phy}; + up = mg_phy_up(&phy, d->phy_addr, &full_duplex, &speed); + if ((ifp->state == MG_TCPIP_STATE_DOWN) && up) { + // link state just went up + MG_DEBUG(("Link is %uM %s-duplex", speed == MG_PHY_SPEED_10M ? 10 : 100, + full_duplex ? "full" : "half")); + } + return up; +} +#pragma CODE_STATE(EMAC_TX_IRQHandler, 32) +#pragma INTERRUPT(EMAC_TX_IRQHandler, IRQ) +void EMAC_TX_IRQHandler(void) { + uint32_t status = EMAC_CTRL->C0TXSTAT; + if (status & 1) { // interrupt caused on channel 0 + while(s_txdesc[s_txno][3] & SWAP32(MG_BIT(29))) (void) 0; + EMAC->TXCP[0] = (uint32_t) &s_txdesc[s_txno][0]; + } + //Write the DMA end of interrupt vector + EMAC->MACEOIVECTOR = 2; +} +static uint32_t s_rxno; +#pragma CODE_STATE(EMAC_RX_IRQHandler, 32) +#pragma INTERRUPT(EMAC_RX_IRQHandler, IRQ) +void EMAC_RX_IRQHandler(void) { + uint32_t status = EMAC_CTRL->C0RXSTAT; + if (status & 1) { // Frame received, loop + uint32_t i; + //MG_INFO(("RX interrupt")); + for (i = 0; i < 10; i++) { // read as they arrive but not forever + if ((s_rxdesc[s_rxno][3] & SWAP32(MG_BIT(29))) == 0) { + uint32_t len = SWAP32(s_rxdesc[s_rxno][3]) & 0xffff; + //MG_INFO(("recv len: %d", len)); + //mg_hexdump(s_rxbuf[s_rxno], len); + mg_tcpip_qwrite(s_rxbuf[s_rxno], len > 4 ? len - 4 : len, s_ifp); + uint32_t flags = s_rxdesc[s_rxno][3]; + s_rxdesc[s_rxno][3] = SWAP32(MG_BIT(29)); + s_rxdesc[s_rxno][2] = SWAP32(ETH_PKT_SIZE); + EMAC->RXCP[0] = (uint32_t) &s_rxdesc[s_rxno][0]; + if (flags & SWAP32(MG_BIT(28))) { + //MG_INFO(("EOQ detected")); + EMAC->RXHDP[0] = (uint32_t) &s_rxdesc[0][0]; + } + } + if (++s_rxno >= ETH_DESC_CNT) s_rxno = 0; + } + } + //Write the DMA end of interrupt vector + EMAC->MACEOIVECTOR = 1; +} +struct mg_tcpip_driver mg_tcpip_driver_tms570 = {mg_tcpip_driver_tms570_init, + mg_tcpip_driver_tms570_tx, NULL, + mg_tcpip_driver_tms570_up}; +#endif + + #ifdef MG_ENABLE_LINES #line 1 "src/drivers/w5500.c" #endif diff --git a/dist/mongoose/mongoose.h b/dist/mongoose/mongoose.h index 31bb7379c..42bf7a345 100644 --- a/dist/mongoose/mongoose.h +++ b/dist/mongoose/mongoose.h @@ -20,7 +20,7 @@ #ifndef MONGOOSE_H #define MONGOOSE_H -#define MG_VERSION "7.15" +#define MG_VERSION "7.16" #ifdef __cplusplus extern "C" { @@ -38,7 +38,7 @@ extern "C" { #define MG_ARCH_NEWLIB 8 // Bare metal ARM #define MG_ARCH_CMSIS_RTOS1 9 // CMSIS-RTOS API v1 (Keil RTX) #define MG_ARCH_TIRTOS 10 // Texas Semi TI-RTOS -#define MG_ARCH_RP2040 11 // Raspberry Pi RP2040 +#define MG_ARCH_PICOSDK 11 // Raspberry Pi Pico-SDK (RP2040, RP2350) #define MG_ARCH_ARMCC 12 // Keil MDK-Core with Configuration Wizard #define MG_ARCH_CMSIS_RTOS2 13 // CMSIS-RTOS API v2 (Keil RTX5, FreeRTOS) #define MG_ARCH_RTTHREAD 14 // RT-Thread RTOS @@ -243,7 +243,7 @@ static inline int mg_mkdir(const char *path, mode_t mode) { #endif -#if MG_ARCH == MG_ARCH_RP2040 +#if MG_ARCH == MG_ARCH_PICOSDK #include #include #include @@ -254,7 +254,16 @@ static inline int mg_mkdir(const char *path, mode_t mode) { #include #include +#include int mkdir(const char *, mode_t); + +#if MG_OTA == MG_OTA_PICOSDK +#include +#if PICO_RP2040 +#include +#endif +#endif + #endif @@ -415,6 +424,10 @@ static inline int mg_mkdir(const char *path, mode_t mode) { #if MG_ARCH == MG_ARCH_WIN32 +#ifndef _CRT_RAND_S +#define _CRT_RAND_S +#endif + #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif @@ -470,11 +483,6 @@ typedef enum { false = 0, true = 1 } bool; #endif #include #pragma comment(lib, "advapi32.lib") -#else -#include -#if defined(_MSC_VER) -#pragma comment(lib, "bcrypt.lib") -#endif #endif // Protect from calls like std::snprintf in app code @@ -2642,82 +2650,59 @@ void mg_rpc_list(struct mg_rpc_req *r); -#define MG_OTA_NONE 0 // No OTA support -#define MG_OTA_FLASH 1 // OTA via an internal flash -#define MG_OTA_ESP32 2 // ESP32 OTA implementation -#define MG_OTA_CUSTOM 100 // Custom implementation +#define MG_OTA_NONE 0 // No OTA support +#define MG_OTA_STM32H5 1 // STM32 H5 +#define MG_OTA_STM32H7 2 // STM32 H7 +#define MG_OTA_STM32H7_DUAL_CORE 3 // STM32 H7 dual core +#define MG_OTA_STM32F 4 // STM32 F7/F4/F2 +#define MG_OTA_CH32V307 100 // WCH CH32V307 +#define MG_OTA_U2A 200 // Renesas U2A16, U2A8, U2A6 +#define MG_OTA_RT1020 300 // IMXRT1020 +#define MG_OTA_RT1060 301 // IMXRT1060 +#define MG_OTA_RT1064 302 // IMXRT1064 +#define MG_OTA_RT1170 303 // IMXRT1170 +#define MG_OTA_MCXN 310 // MCXN947 +#define MG_OTA_FLASH 900 // OTA via an internal flash +#define MG_OTA_ESP32 910 // ESP32 OTA implementation +#define MG_OTA_PICOSDK 920 // RP2040/2350 using Pico-SDK hardware_flash +#define MG_OTA_CUSTOM 1000 // Custom implementation #ifndef MG_OTA #define MG_OTA MG_OTA_NONE -#endif - -#if defined(__GNUC__) && !defined(__APPLE__) -#define MG_IRAM __attribute__((section(".iram"))) +#else +#ifndef MG_IRAM +#if defined(__GNUC__) +#define MG_IRAM __attribute__((noinline, section(".iram"))) #else #define MG_IRAM -#endif +#endif // compiler +#endif // IRAM +#endif // OTA // Firmware update API bool mg_ota_begin(size_t new_firmware_size); // Start writing bool mg_ota_write(const void *buf, size_t len); // Write chunk, aligned to 1k bool mg_ota_end(void); // Stop writing -enum { - MG_OTA_UNAVAILABLE = 0, // No OTA information is present - MG_OTA_FIRST_BOOT = 1, // Device booting the first time after the OTA - MG_OTA_UNCOMMITTED = 2, // Ditto, but marking us for the rollback - MG_OTA_COMMITTED = 3 // The firmware is good -}; -enum { MG_FIRMWARE_CURRENT = 0, MG_FIRMWARE_PREVIOUS = 1 }; - -int mg_ota_status(int firmware); // Return firmware status MG_OTA_* -uint32_t mg_ota_crc32(int firmware); // Return firmware checksum -uint32_t mg_ota_timestamp(int firmware); // Firmware timestamp, UNIX UTC epoch -size_t mg_ota_size(int firmware); // Firmware size - -bool mg_ota_commit(void); // Commit current firmware -bool mg_ota_rollback(void); // Rollback to the previous firmware -MG_IRAM void mg_ota_boot(void); // Bootloader function -// Copyright (c) 2023 Cesanta Software Limited -// All rights reserved - +#if MG_OTA != MG_OTA_NONE && MG_OTA != MG_OTA_CUSTOM +struct mg_flash { + void *start; // Address at which flash starts + size_t size; // Flash size + size_t secsz; // Sector size + size_t align; // Write alignment + bool (*write_fn)(void *, const void *, size_t); // Write function + bool (*swap_fn)(void); // Swap partitions +}; -#define MG_DEVICE_NONE 0 // Dummy system - -#define MG_DEVICE_STM32H5 1 // STM32 H5 -#define MG_DEVICE_STM32H7 2 // STM32 H7 -#define MG_DEVICE_CH32V307 100 // WCH CH32V307 -#define MG_DEVICE_U2A 200 // Renesas U2A16, U2A8, U2A6 -#define MG_DEVICE_RT1020 300 // IMXRT1020 -#define MG_DEVICE_RT1060 301 // IMXRT1060 -#define MG_DEVICE_CUSTOM 1000 // Custom implementation +bool mg_ota_flash_begin(size_t new_firmware_size, struct mg_flash *flash); +bool mg_ota_flash_write(const void *buf, size_t len, struct mg_flash *flash); +bool mg_ota_flash_end(struct mg_flash *flash); -#ifndef MG_DEVICE -#define MG_DEVICE MG_DEVICE_NONE #endif -// Flash information -void *mg_flash_start(void); // Return flash start address -size_t mg_flash_size(void); // Return flash size -size_t mg_flash_sector_size(void); // Return flash sector size -size_t mg_flash_write_align(void); // Return flash write align, minimum 4 -int mg_flash_bank(void); // 0: not dual bank, 1: bank1, 2: bank2 - -// Write, erase, swap bank -bool mg_flash_write(void *addr, const void *buf, size_t len); -bool mg_flash_erase(void *sector); -bool mg_flash_swap_bank(void); - -// Convenience functions to store data on a flash sector with wear levelling -// If `sector` is NULL, then the last sector of flash is used -bool mg_flash_load(void *sector, uint32_t key, void *buf, size_t len); -bool mg_flash_save(void *sector, uint32_t key, const void *buf, size_t len); - -void mg_device_reset(void); // Reboot device immediately - @@ -2792,6 +2777,7 @@ void mg_tcpip_arp_request(struct mg_tcpip_if *ifp, uint32_t ip, uint8_t *mac); extern struct mg_tcpip_driver mg_tcpip_driver_stm32f; extern struct mg_tcpip_driver mg_tcpip_driver_w5500; extern struct mg_tcpip_driver mg_tcpip_driver_tm4c; +extern struct mg_tcpip_driver mg_tcpip_driver_tms570; extern struct mg_tcpip_driver mg_tcpip_driver_stm32h; extern struct mg_tcpip_driver mg_tcpip_driver_imxrt; extern struct mg_tcpip_driver mg_tcpip_driver_same54; @@ -3130,22 +3116,47 @@ struct mg_tcpip_driver_tm4c_data { #endif -#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_XMC) && MG_ENABLE_DRIVER_XMC +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_TMS570) && MG_ENABLE_DRIVER_TMS570 +struct mg_tcpip_driver_tms570_data { + int mdc_cr; + int phy_addr; +}; -struct mg_tcpip_driver_xmc_data { - // 13.2.8.1 Station Management Functions - // MDC clock divider (). MDC clock is derived from ETH MAC clock - // It must not exceed 2.5MHz - // ETH Clock range DIVIDER mdc_cr VALUE - // -------------------------------------------- - // -1 <-- tell driver to guess the value - // 60-100 MHz ETH Clock/42 0 - // 100-150 MHz ETH Clock/62 1 - // 20-35 MHz ETH Clock/16 2 - // 35-60 MHz ETH Clock/26 3 - // 150-250 MHz ETH Clock/102 4 - // 250-300 MHz ETH Clock/124 5 - // 110, 111 Reserved +#ifndef MG_TCPIP_PHY_ADDR +#define MG_TCPIP_PHY_ADDR 0 +#endif + +#ifndef MG_DRIVER_MDC_CR +#define MG_DRIVER_MDC_CR 1 +#endif + +#define MG_TCPIP_DRIVER_INIT(mgr) \ + do { \ + static struct mg_tcpip_driver_tms570_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + driver_data_.mdc_cr = MG_DRIVER_MDC_CR; \ + driver_data_.phy_addr = MG_TCPIP_PHY_ADDR; \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.driver = &mg_tcpip_driver_tms570; \ + mif_.driver_data = &driver_data_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mg_tcpip_init(mgr, &mif_); \ + MG_INFO(("Driver: tms570, MAC: %M", mg_print_mac, mif_.mac));\ + } while (0) +#endif + + + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_W5500) && MG_ENABLE_DRIVER_W5500 + +#endif + + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_XMC7) && MG_ENABLE_DRIVER_XMC7 + +struct mg_tcpip_driver_xmc7_data { int mdc_cr; // Valid values: -1, 0, 1, 2, 3, 4, 5 uint8_t phy_addr; }; @@ -3155,31 +3166,45 @@ struct mg_tcpip_driver_xmc_data { #endif #ifndef MG_DRIVER_MDC_CR -#define MG_DRIVER_MDC_CR 4 +#define MG_DRIVER_MDC_CR 3 #endif #define MG_TCPIP_DRIVER_INIT(mgr) \ do { \ - static struct mg_tcpip_driver_xmc_data driver_data_; \ + static struct mg_tcpip_driver_xmc7_data driver_data_; \ static struct mg_tcpip_if mif_; \ driver_data_.mdc_cr = MG_DRIVER_MDC_CR; \ driver_data_.phy_addr = MG_TCPIP_PHY_ADDR; \ mif_.ip = MG_TCPIP_IP; \ mif_.mask = MG_TCPIP_MASK; \ mif_.gw = MG_TCPIP_GW; \ - mif_.driver = &mg_tcpip_driver_xmc; \ + mif_.driver = &mg_tcpip_driver_xmc7; \ mif_.driver_data = &driver_data_; \ MG_SET_MAC_ADDRESS(mif_.mac); \ mg_tcpip_init(mgr, &mif_); \ - MG_INFO(("Driver: xmc, MAC: %M", mg_print_mac, mif_.mac)); \ + MG_INFO(("Driver: xmc7, MAC: %M", mg_print_mac, mif_.mac)); \ } while (0) #endif -#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_XMC7) && MG_ENABLE_DRIVER_XMC7 -struct mg_tcpip_driver_xmc7_data { +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_XMC) && MG_ENABLE_DRIVER_XMC + +struct mg_tcpip_driver_xmc_data { + // 13.2.8.1 Station Management Functions + // MDC clock divider (). MDC clock is derived from ETH MAC clock + // It must not exceed 2.5MHz + // ETH Clock range DIVIDER mdc_cr VALUE + // -------------------------------------------- + // -1 <-- tell driver to guess the value + // 60-100 MHz ETH Clock/42 0 + // 100-150 MHz ETH Clock/62 1 + // 20-35 MHz ETH Clock/16 2 + // 35-60 MHz ETH Clock/26 3 + // 150-250 MHz ETH Clock/102 4 + // 250-300 MHz ETH Clock/124 5 + // 110, 111 Reserved int mdc_cr; // Valid values: -1, 0, 1, 2, 3, 4, 5 uint8_t phy_addr; }; @@ -3189,28 +3214,27 @@ struct mg_tcpip_driver_xmc7_data { #endif #ifndef MG_DRIVER_MDC_CR -#define MG_DRIVER_MDC_CR 3 +#define MG_DRIVER_MDC_CR 4 #endif #define MG_TCPIP_DRIVER_INIT(mgr) \ do { \ - static struct mg_tcpip_driver_xmc7_data driver_data_; \ + static struct mg_tcpip_driver_xmc_data driver_data_; \ static struct mg_tcpip_if mif_; \ driver_data_.mdc_cr = MG_DRIVER_MDC_CR; \ driver_data_.phy_addr = MG_TCPIP_PHY_ADDR; \ mif_.ip = MG_TCPIP_IP; \ mif_.mask = MG_TCPIP_MASK; \ mif_.gw = MG_TCPIP_GW; \ - mif_.driver = &mg_tcpip_driver_xmc7; \ + mif_.driver = &mg_tcpip_driver_xmc; \ mif_.driver_data = &driver_data_; \ MG_SET_MAC_ADDRESS(mif_.mac); \ mg_tcpip_init(mgr, &mif_); \ - MG_INFO(("Driver: xmc7, MAC: %M", mg_print_mac, mif_.mac)); \ + MG_INFO(("Driver: xmc, MAC: %M", mg_print_mac, mif_.mac)); \ } while (0) #endif - #ifdef __cplusplus } #endif diff --git a/dist/sds/sds.c b/dist/sds/sds.c index df2e4bacc..3fca1b41d 100644 --- a/dist/sds/sds.c +++ b/dist/sds/sds.c @@ -1290,8 +1290,9 @@ sds *sdssplitargs(const char *line, int *argc) { if (*p) p++; } /* add the token to the vector */ - vector = s_realloc(vector,((*argc)+1)*sizeof(char*)); - if (vector == NULL) goto err; + char **tmpvector = s_realloc(vector,((*argc)+1)*sizeof(char*)); + if (tmpvector == NULL) goto err; + vector = tmpvector; vector[*argc] = current; (*argc)++; current = NULL; @@ -1373,6 +1374,16 @@ void sds_free(void *ptr) { s_free(ptr); } #include "testhelp.h" #include "limits.h" +#if defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32) +// On Windows the default stack size is 1MB, so tests need +// to be run with reduced stack space requirements. +#define STACK_BUF_SIZE (256*1024) +#define STACK_BUF_SIZE_TEXT "256kB" +#else +#define STACK_BUF_SIZE (1024*1024) +#define STACK_BUF_SIZE_TEXT "1MB" +#endif + #define UNUSED(x) (void)(x) int sdsTest(void) { { @@ -1411,12 +1422,12 @@ int sdsTest(void) { { sdsfree(x); - char etalon[1024*1024]; + char etalon[STACK_BUF_SIZE]; for (size_t i = 0; i < sizeof(etalon); i++) { etalon[i] = '0'; } x = sdscatprintf(sdsempty(),"%0*d",(int)sizeof(etalon),0); - test_cond("sdscatprintf() can print 1MB", + test_cond("sdscatprintf() can print " STACK_BUF_SIZE_TEXT, sdslen(x) == sizeof(etalon) && memcmp(x,etalon,sizeof(etalon)) == 0) } diff --git a/dist/sds/sds.h b/dist/sds/sds.h index 8aebe1c58..7cf552b84 100644 --- a/dist/sds/sds.h +++ b/dist/sds/sds.h @@ -40,39 +40,56 @@ extern const char *SDS_NOINIT; #include #include +#ifdef _MSC_VER +#include +#define ssize_t SSIZE_T + +#define SDS_PACKED_PRE __pragma(pack(push, 1)) +#define SDS_PACKED_ATTR +#define SDS_PACKED_POST __pragma( pack(pop)) +#else // _MSC_VER +#define SDS_PACKED_PRE +#define SDS_PACKED_ATTR __attribute__((__packed__)) +#define SDS_PACKED_POST +#endif // _MSC_VER + typedef char *sds; +SDS_PACKED_PRE + /* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ -struct __attribute__ ((__packed__)) sdshdr5 { - unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ - char buf[]; +struct SDS_PACKED_ATTR sdshdr5 { + unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ + char buf[]; }; -struct __attribute__ ((__packed__)) sdshdr8 { - uint8_t len; /* used */ - uint8_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; +struct SDS_PACKED_ATTR sdshdr8 { + uint8_t len; /* used */ + uint8_t alloc; /* excluding the header and null terminator */ + unsigned char flags; /* 3 lsb of type, 5 unused bits */ + char buf[]; }; -struct __attribute__ ((__packed__)) sdshdr16 { - uint16_t len; /* used */ - uint16_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; +struct SDS_PACKED_ATTR sdshdr16 { + uint16_t len; /* used */ + uint16_t alloc; /* excluding the header and null terminator */ + unsigned char flags; /* 3 lsb of type, 5 unused bits */ + char buf[]; }; -struct __attribute__ ((__packed__)) sdshdr32 { - uint32_t len; /* used */ - uint32_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; +struct SDS_PACKED_ATTR sdshdr32 { + uint32_t len; /* used */ + uint32_t alloc; /* excluding the header and null terminator */ + unsigned char flags; /* 3 lsb of type, 5 unused bits */ + char buf[]; }; -struct __attribute__ ((__packed__)) sdshdr64 { - uint64_t len; /* used */ - uint64_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; +struct SDS_PACKED_ATTR sdshdr64 { + uint64_t len; /* used */ + uint64_t alloc; /* excluding the header and null terminator */ + unsigned char flags; /* 3 lsb of type, 5 unused bits */ + char buf[]; }; +SDS_PACKED_POST + #define SDS_TYPE_5 0 #define SDS_TYPE_8 1 #define SDS_TYPE_16 2 diff --git a/docs/050-scripting/functions/index.md b/docs/050-scripting/functions/index.md index 2942277ce..69c481d1b 100644 --- a/docs/050-scripting/functions/index.md +++ b/docs/050-scripting/functions/index.md @@ -9,6 +9,7 @@ List of myMPD specific Lua functions. | [json.decode](json.md) | Parses a Json string to a Lua table. | | [json.encode](json.md) | Encodes a Lua table as Json string. | | [mympd.api](mympd_api.md) | Access to the myMPD API. | +| [mympd.api_partition](mympd_api.md) | Access to the myMPD API. | | [mympd.cache_cover_write](diskcache.md) | Writes a cover cache file. | | [mympd.cache_lyrics_write](diskcache.md) | Writes a lyrics cache file. | | [mympd.cache_thumbs_write](diskcache.md) | Writes a thumbs cache file. | diff --git a/docs/050-scripting/functions/mympd_api.md b/docs/050-scripting/functions/mympd_api.md index 1d20809f2..a7eb22358 100644 --- a/docs/050-scripting/functions/mympd_api.md +++ b/docs/050-scripting/functions/mympd_api.md @@ -5,15 +5,20 @@ title: Accessing the myMPD API Calls the myMPD API, look at [API](../../060-references/api/index.md) for detailed API description. ```lua +-- Call myMPD API for current partition local rc, result = mympd.api("method", params) + +-- Call myMPD API for another partition +local rc, result = mympd.api_partition("default", "method", params) ``` **Parameters:** | PARAMETER | TYPE | DESCRIPTION | | --------- | ---- | ----------- | +| partition | string | MPD partition | | method | string | myMPD API method | -| params | lua table | the jsonrpc parameters | +| params | lua table | The jsonrpc parameters as Lua table | **Returns:** diff --git a/docs/060-references/local-playback.md b/docs/060-references/local-playback.md index 65fefbb74..8027c7a5a 100644 --- a/docs/060-references/local-playback.md +++ b/docs/060-references/local-playback.md @@ -25,3 +25,6 @@ audio_output { tags "yes" } ``` + +!!! note + Playback does not start instantinously, buffering can take up to 30 seconds. diff --git a/docs/060-references/smart-playlists.md b/docs/060-references/smart-playlists.md index 7d3c28bc4..d0a9265ec 100644 --- a/docs/060-references/smart-playlists.md +++ b/docs/060-references/smart-playlists.md @@ -28,7 +28,7 @@ Smart playlists are saved in the folder `/var/lib/mympd/smartpls` (one JSON file ### Sticker based ``` json -{"type": "sticker", "sticker": "like", "value": "2", "op": "=" "sort": "", "sortdesc": false, "maxentries": 200} +{"type": "sticker", "sticker": "like", "value": "2", "op": "=", "sort": "", "sortdesc": false, "maxentries": 200} ``` ### Newest songs diff --git a/docs/_includes/translating_status.md b/docs/_includes/translating_status.md index 82c2ffd1d..2842ae47f 100644 --- a/docs/_includes/translating_status.md +++ b/docs/_includes/translating_status.md @@ -1,14 +1,14 @@ -- bg-BG: 1089 missing phrases +- bg-BG: 1090 missing phrases - es-AR: fully translated -- es-ES: 956 missing phrases -- es-VE: 944 missing phrases -- fi-FI: 941 missing phrases +- es-ES: 957 missing phrases +- es-VE: 945 missing phrases +- fi-FI: 942 missing phrases - fr-FR: fully translated - it-IT: fully translated - ja-JP: fully translated - ko-KR: fully translated - nl-NL: fully translated -- pl-PL: 89 missing phrases -- ru-RU: 6 missing phrases +- pl-PL: 90 missing phrases +- ru-RU: 7 missing phrases - zh-Hans: fully translated -- zh-Hant: 123 missing phrases +- zh-Hant: 124 missing phrases diff --git a/docs/_includes/version b/docs/_includes/version index e1dc037df..2941b8199 100644 --- a/docs/_includes/version +++ b/docs/_includes/version @@ -1 +1 @@ -18.2.2 \ No newline at end of file +19.0.0 \ No newline at end of file diff --git a/htdocs/css/mympd.css b/htdocs/css/mympd.css index c131c18e6..98648386a 100644 --- a/htdocs/css/mympd.css +++ b/htdocs/css/mympd.css @@ -407,6 +407,10 @@ th, word-break: keep-all; } +.viewListItem [data-col="Duration"] { + max-width: none; +} + [data-col="Type"] { width: 3rem; word-break: keep-all; @@ -566,22 +570,29 @@ div#homeActions { padding: 0.5rem 0; } -.cover-grid { - background-size: cover; - background-position: center center; - overflow: hidden; - width: var(--grid-thumbnail-size); - height: var(--grid-thumbnail-size); - max-width: 100%; - padding: 0.5rem; +.cover-grid > img { + object-fit: cover; +} + +.list-image { + width: 6rem; + max-width: 6rem; + height: 6rem; + margin-right: 1rem; +} + +.list-image > img { + object-fit: cover; + width: 6rem; + height: 6rem; + border-radius: 0.375rem; } .thumbnail { - background-size: cover; - background-position: center center; - overflow: hidden; + object-fit: cover; width: 2rem; height: 2rem; + border-radius: 0.2rem; } @media only screen and (300px <= width <= 400px) { @@ -1355,14 +1366,16 @@ table div.alert { background-color: var(--bs-alert-bg); } -.list-image { - background-size: cover; - width: 6rem; - max-width: 6rem; - height: 6rem; - border-radius: 0.375rem -} - .colMaxContent { max-width:fit-content; } + +.listThumbnailBadge { + position: relative; + top: -1.4rem; + font-size: 80%; +} + +.viewListItem { + background-color: transparent; +} diff --git a/htdocs/index.html b/htdocs/index.html index 6326fd117..d2ddebc8d 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -1383,15 +1383,7 @@