Skip to content

Preserve sub-millisecond event timestamp precision - #7

Open
bmehta001 wants to merge 28 commits into
mainfrom
bhamehta/fix-event-timestamp-precision
Open

Preserve sub-millisecond event timestamp precision#7
bmehta001 wants to merge 28 commits into
mainfrom
bhamehta/fix-event-timestamp-precision

Conversation

@bmehta001

Copy link
Copy Markdown
Owner

Summary

  • preserve nanosecond-derived 100 ns precision for default POSIX event timestamps
  • use GetSystemTimePreciseAsFileTime on Windows when available, with a Windows 7-compatible fallback
  • add regression coverage for POSIX timestamps retaining sub-millisecond ticks

Fixes microsoft#1514.

Validation

  • git diff --check passed
  • Fresh Windows CMake configuration succeeded
  • The unit-test build was blocked by existing configuration issues: missing gtest/gtest.lib and /WX failures from pre-existing HAVE_MAT_AI macro redefinition / exception-mode warnings

mogiligarimidi23 and others added 28 commits June 23, 2026 00:22
)

GUID_t::operator< used a non-lexicographic chained-|| comparison with a
`Data3 == other.Data3` typo (should be `<`):

    return Data1 < other.Data1 || Data2 < other.Data2 ||
           Data3 == other.Data3 || (memcmp(Data4,...) < 0);

Two defects: (1) the Data3 line uses == instead of <, so GUIDs differing
only in Data3 compare equivalent; (2) the chained-|| form is not
lexicographic and violates antisymmetry (e.g. {1,5,..} and {2,3,..} can
report both a<b and b<a). Using this as the ordering for std::set<GUID_t>
/ std::map<GUID_t,...> (the default std::less calls it; the comment says
it is "needed for maps") is undefined behavior -- container corruption,
infinite loops, or crashes. GUID_t is a public header type
(EventProperty.hpp), so SDK consumers hit this with default ordered
containers.

Replace with proper lexicographic comparison (Data1, then Data2, then
Data3, then memcmp(Data4)).

Test: GuidTests.OperatorLess_IsStrictWeakOrdering (added) fails on the
old operator (c<d == false; std::set of 4 distinct GUIDs collapses to 3)
and passes after the fix. Verified by building and running UnitTests on
Linux (host): all 12 GuidTests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…adBlob (microsoft#1492)

Under USE_ONEDS_BOUNDCHECK_METHODS, ReadBlob assigned the errno_t return
of MAT::BoundCheckFunctions::oneds_memcpy_s (0 == success) directly to a
bool:

    bool result = MAT::BoundCheckFunctions::oneds_memcpy_s(...);

so `result` is false on success and true on failure -- inverted. ReadBlob
(and thus ReadFloat/ReadDouble, which call it) returns "failure" for every
successful read, breaking decode of any payload containing a
blob/float/double field whenever the SDK is built with bound-check
methods. The non-boundcheck branch one line below already does it
correctly: `bool result = (memcpy_s(...) == 0);`.

Compare the result against 0, matching the memcpy_s branch.

Validated (TDD) with a standalone reader round-trip compiled with
-DHAVE_ONEDS_BOUNDCHECK_METHODS: ReadDouble of a written double returns
false before the fix and true (correct value) after. The bug is gated
behind the non-default USE_ONEDS_BOUNDCHECK_METHODS option; the default
build is unaffected (the changed line is #ifdef'd out) and the existing
bondlite CompactBinaryProtocolTests assert ReadBlob == true under that
option.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Update EventPropertiesDecorator.hpp

Scrub IP addresses by default.

* Gate IP scrubbing behind CFG_BOOL_ENABLE_IP_SCRUBBING (default on)

The initial change set RECORD_FLAGS_EVENTTAG_SCRUB_IP unconditionally for
every event, forcing IP scrubbing on all SDK consumers in direct-upload
mode -- a breaking change for apps that need client IP (e.g. geo-location).

Make scrubbing the default but opt-out: the decorator sets the SCRUB_IP
record flag unless CFG_BOOL_ENABLE_IP_SCRUBBING is explicitly set to false.
record.flags is forwarded on the cross-platform/direct-upload path, so this
redacts client IP at the collector without relying on ext.metadata privacy
tags.

- ILogConfiguration.hpp: add CFG_BOOL_ENABLE_IP_SCRUBBING config key
- EventPropertiesDecorator.hpp: gate SCRUB_IP flag behind the config
- EventPropertiesDecoratorTests.cpp: add default-on, opt-out, explicit-enable
  tests with a per-instance ConfigurableLogManager helper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Cover SDK stats events and add wrapper-parity config keys for IP scrubbing

- Statistics.cpp: SDK statistics/metastats events bypass EventPropertiesDecorator,
  so apply the same collector-side client-IP scrub (gated by
  CFG_BOOL_ENABLE_IP_SCRUBBING, on by default) to those records too. Closes the
  gap identified in PR review.
- LogConfigurationKey.java + ODWLogConfiguration.{h,mm}: expose
  CFG_BOOL_ENABLE_IP_SCRUBBING ('enableIpScrubbing') to the Android (Java) and
  Apple (Obj-C) wrappers for API parity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify CFG_BOOL_ENABLE_IP_SCRUBBING docstring (Copilot round 1)

The doc implied the setting only applies in direct-upload mode, but the scrub
flag is set for all events and modes. Clarify that the flag is honored by the
OneCollector direct-upload path while UTC mode applies its own client-privacy
handling. No behavior change -- the flag is intentionally mode-agnostic and is
ignored by the UTC pipeline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot round 2 on microsoft#1161: self-contained header + default-config docs

- EventPropertiesDecorator.hpp: include ILogManager.hpp so the header is
  self-contained for the ILogManager (m_owner) and ILogConfiguration types /
  CFG_BOOL_ENABLE_IP_SCRUBBING used inline, instead of relying on the includer.
- Clarify CFG_BOOL_ENABLE_IP_SCRUBBING docs (C++ / Java / Obj-C): scrubbing is
  applied unless explicitly set to false (on by default) and the key is not
  present in the default configuration, so GetDefaultConfiguration() does not
  surface it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot round 3 on microsoft#1161: extract on-wire record flags to a shared header

Move the RECORD_FLAGS_EVENTTAG_* on-wire bits out of EventPropertiesDecorator.hpp
into a dedicated decorators/RecordFlagConstants.hpp, exposed as static constexpr
std::int64_t in the MAT namespace (no longer #define macros). This lets the stats
pipeline reference RECORD_FLAGS_EVENTTAG_SCRUB_IP via the small shared header
instead of pulling in the full decorator header, and avoids macro pollution.

- New: lib/decorators/RecordFlagConstants.hpp
- EventPropertiesDecorator.hpp: include the shared header; drop the macros
- Statistics.cpp: include the shared header instead of EventPropertiesDecorator.hpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden OfflineStorage_Room against null JNI objects (no-crash on null m_room/element)

Defensive follow-up to the microsoft#1227 family of Android Room crashes ("java_object ==
null in call to GetObjectClass" under GetAndReserveRecords). microsoft#1417 fixed the
stale-local-ref root cause but left two null paths unguarded, which can still
hard-abort the host process:

  * m_room is null when the Room DB failed to open or was torn down (the
    destructor already guards with `if (s_vm && m_room)`, but ~10 other methods
    dereferenced it unconditionally). Add `if (!m_room) return <fail>;` guards to
    DeleteRecords(x2), GetAndReserveRecords, ReleaseRecords, StoreRecords,
    DeleteSetting, StoreSetting, GetSizeInternal, GetRecordCount,
    ResizeDbInternal, and GetRecords. GetSetting already had this guard.
  * a null element in the getAndReserve/releaseRecords result array (observed to
    be androidx.room-version sensitive) was passed to GetObjectClass. Guard both
    loops: GetAndReserveRecords pops the frame and stops (the existing
    index < limit path releases the rest for retry); ReleaseRecords skips it.

Turns a process-killing JNI abort into graceful degradation. No functional change
on the healthy path. Compiles on Android only (JNI/Room) -- validated by CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: note the androidx.room version the SDK is built against (microsoft#1227 follow-up)

cpp-start-android.md covered Room setup but said nothing about the Room version.
The GetAndReserveRecords native crash (microsoft#1227, Room-version-sensitive) and the
null-guard hardening in this PR make this worth documenting: the maesdk AAR
brings androidx.room transitively (pinned in maesdk/build.gradle, currently
2.8.4); since Gradle resolves one Room version app-wide, consumers should not
force a version below what the SDK is built against and should prefer aligning
on the bundled (or a compatible newer) version. Points at build.gradle as the
source of truth so the doc doesn't drift on future bumps.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Room: guard null JNIEnv in 5 JNI methods; skip releaseUnconsumed on null element

Address the latest Copilot review (6 comments) on
lib/offline/OfflineStorage_Room.cpp.

ConnectedEnv null-env guards (5): DeleteByToken, ReleaseRecords, DeleteSetting,
StoreSetting and GetRecords created ConnectedEnv env(s_vm) and dereferenced
env->... behind only an if(!m_room) guard. ConnectedEnv::operator! can report a
null JNIEnv (null s_vm / thread-attach failure), and sibling methods already
guard with if(!env); added the matching early return (void/false/records) so a
null env no longer crashes.

releaseUnconsumed on null element: the null-array-element path broke out and
fell through to releaseUnconsumed(selected, index). The Java
StorageRecordDao.releaseUnconsumed (pre-existing since 2020, commit c81d46a)
reads selected[0..unconsumed-1] ignoring the offset, so on the null path it
could index the null element and throw, or release the wrong rows. A
sawNullElement flag now skips releaseUnconsumed on that path; the reserved
records expire and are retried (no data loss), and the normal end-early path is
unchanged.

Validated: NDK aarch64-linux-android23 -fsyntax-only clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Lalit Kumar Bhasin <labhas@microsoft.com>
Co-authored-by: bmehta001 <bmehta001@users.noreply.github.com>
Co-authored-by: Bhagirath Mehta <bhamehta@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nary-footprint reductions (microsoft#1475)

* Add vcpkg-release-bump workflow to automate port version bumps

On a published version release, open a PR to microsoft/vcpkg bumping the
cpp-client-telemetry port (REF -> tag, recomputed SHA512, version, then
x-add-version). Runs only on published, non-prerelease version tags
(vX.Y.Z.W) or manual dispatch, and opens no PR when the port already
matches the release. Requires repo variable VCPKG_FORK_REPO and secret
VCPKG_BUMP_TOKEN.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump in-repo overlay port to v3.10.161.1 tag

Repoint tools/ports/cpp-client-telemetry REF from the pre-release commit to
the published v3.10.161.1 tag (SHA512 updated) for exact parity with the
official microsoft/vcpkg port. Version was already 3.10.161.1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(vcpkg): add manifest-mode overlay fallback for pre-registry installs

building-with-vcpkg.md only told manifest-mode users to add
`cpp-client-telemetry` to vcpkg.json, which fails with an unknown-port
error until the port is accepted into the official vcpkg registry.
Document the `vcpkg-configuration.json` `overlay-ports` fallback that
points manifest mode at the in-repo overlay port, giving parity with the
classic-mode `--overlay-ports` instructions already in the doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* vcpkg-bump: address Copilot force-with-lease comment + drop docs note

.github/workflows/vcpkg-release-bump.yml (Copilot): `git push
--force-with-lease` could fail on reruns because a fresh clone has no
remote-tracking ref for an already-existing bump branch, so the workflow
couldn't refresh an open bump PR (contradicting the "force-pushed branch
refreshes it" intent). Fetch the branch into refs/remotes/origin/${BR}
(|| true on the first run, when it doesn't exist yet) before the
force-with-lease push so the lease has a ref to compare against.
  Verified at .github/workflows/vcpkg-release-bump.yml:146-152.

docs/building-with-vcpkg.md: remove the manifest-mode overlay-ports note
added in 136e010, per maintainer request.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update vcpkg docs: port is now live in the official registry

The cpp-client-telemetry port was merged into the upstream vcpkg registry
(microsoft/vcpkg#52316, version 3.10.161.1), so docs/building-with-vcpkg.md
no longer needs the conditional "once the port is accepted" phrasing.

- Intro: state the port is published in the official registry and consumable
  directly; drop the stale "build recipe / CONTROL file" wording (vcpkg uses
  vcpkg.json, and the port is registry-resolved now).
- "Installing from the vcpkg registry": present tense, link to the upstream
  ports/cpp-client-telemetry directory.
- "Installing from the overlay port": reframe as development-only (test local
  port changes or a newer SDK revision before they reach the registry).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Align overlay port with the merged upstream vcpkg port

The cpp-client-telemetry port merged into microsoft/vcpkg
(ports/cpp-client-telemetry) ships only portfile.cmake + vcpkg.json. Bring the
in-repo overlay back in sync so testing the overlay validates exactly what is
published.

- Drop the custom 'usage' file and its install step in portfile.cmake. The two
  lines it printed (find_package(MSTelemetry CONFIG REQUIRED) +
  target_link_libraries ... MSTelemetry::mat) duplicate vcpkg's auto-generated
  heuristic usage, and the upstream port carries no usage file.
- Reorder vcpkg.json dependencies to vcpkg format-manifest canonical
  (alphabetical) order; same dependency set, no resolution change.

After this, the overlay portfile.cmake and vcpkg.json are byte-identical to the
upstream port blobs (cfdab23 / a74cc08).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix vcpkg-bump PR existence check: jq null skipped PR creation

Copilot review (round): the open-PR existence guard used
--jq '.[0].number'. On the first run, when no PR exists yet, gh pr list
returns [] and .[0].number evaluates to null, which gh prints as the literal
string "null". [ -n "null" ] is true, so the workflow wrongly logged "An open
PR already exists" and skipped 'gh pr create' -- the release-bump PR would
never be opened on a clean run.

Fix: --jq '.[0].number // empty' yields empty output when no PR exists (guard
false -> PR created) and the PR number when one does (guard true -> skipped).

Verified jq semantics (jq 1.x): '[] | .[0].number' -> null (prints "null");
'[] | .[0].number // empty' -> no output; '[{number:42}] | .[0].number // empty'
-> 42. Confirmed at .github/workflows/vcpkg-release-bump.yml:158.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden vcpkg-bump workflow: graceful no-op on non-version tags, no token in URL

Address Copilot round comments on the release-bump workflow.

vcpkg-release-bump.yml:77 - non-version tag handling was contradictory: the
message said "skipping" but the step ran exit 1, failing the workflow. A
release published with a non-4-part tag (the SDK has historical 3-part tags
like v3.3.8) would mark the automatic run red for what should be a no-op. Now:
manual workflow_dispatch with a bad tag still fails loudly (user error), but the
automatic release trigger emits a notice, sets a 'skip' output, and exits 0. All
downstream steps are gated on steps.ver.outputs.skip != 'true'.

vcpkg-release-bump.yml:100 - the PAT was embedded in the clone URL, which
persists it in .git/config and risks leaking if git echoes the remote. Switch to
'gh auth setup-git' (writes a credential helper to the global gitconfig) plus a
tokenless https clone; the later push step reuses that helper via its GH_TOKEN
env. No token appears in any URL or on disk.

Validated: workflow YAML parses (PyYAML) and every embedded run block passes
'bash -n'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* vcpkg-bump workflow: pass release tag via env to prevent shell injection

Code review flagged a GitHub Actions script-injection vector: the "Resolve tag
and version" step interpolated the untrusted release tag directly into the run:
shell, before any validation. A tag containing shell metacharacters (e.g.
v1.0.0.0";id;") would execute at assignment time, before the version regex
runs. Injected code could write to GITHUB_ENV/GITHUB_PATH, which persist into
the later Clone and push/PR steps that carry the VCPKG_BUMP_TOKEN PAT, enabling
token exfiltration.

Fix: pass the tag values through env: (RELEASE_TAG/INPUT_TAG) and reference them
as quoted shell variables (TAG="${RELEASE_TAG:-$INPUT_TAG}"). Env values are not
parsed as shell, so metacharacters can no longer inject. All downstream steps
already use the regex-validated steps.ver.outputs.* values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Enable function-level linking in the CMake build so consumers can dead-strip

The compiler-flags block in CMakeLists.txt is wrapped entirely in
`if(NOT MATSDK_USE_VCPKG_DEPS)`, and its MSVC branch only sets warning flags --
never /Gy. So the CMake/vcpkg build (the one packaged for downstream consumers)
compiles every TU without function-level COMDATs, and referencing one symbol
pulls the whole .obj into the consumer image.

Add a block, applied in BOTH vendored and vcpkg modes, that splits
functions/data into COMDATs/sections (MSVC /Gy /Gw; GCC/Clang
-ffunction-sections -fdata-sections; AppleClang -ffunction-sections). This lets
a consumer's linker dead-strip unreferenced SDK code (/OPT:REF + /OPT:ICF,
--gc-sections, -dead_strip) and matches the MSBuild Release projects, which
already enable FunctionLevelLinking + OptimizeReferences + EnableCOMDATFolding.

No source or ABI change. Bundled into the vcpkg PR since it directly improves
the footprint of the vcpkg-packaged library.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Reword function-level-linking comment to justify the vcpkg-mode exception

Copilot review: the new comment read as conflicting with the earlier
"let the toolchain manage compiler flags" note. Clarify that these section/COMDAT
flags are a deliberate exception -- they are not optimization/dependency choices
the toolchain owns; the toolchain doesn't set them, and without them the
vcpkg-packaged library links whole .obj files. No code change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Consolidate section-splitting flags into the global block (drop REL_FLAGS dupes)

Copilot review: the non-vcpkg REL_FLAGS already injected -ffunction-sections
(and -fdata-sections for GNU), duplicating the new global block on Release
builds. Remove them from REL_FLAGS so the global add_compile_options block is
the single source of truth for section splitting across both dependency modes
(it now also covers MSVC /Gy /Gw and AppleClang, which CI confirms build clean).
No behavior change; eliminates redundant flags and future drift.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* vcpkg: request sqlite3 without default features (drop json1) + docs

The SDK uses SQLite only for offline event storage (plain tables/indexes,
no JSON/FTS/RTREE/vtab), so the port now declares sqlite3 with
default-features:false instead of pulling json1. This is the necessary
floor for footprint-conscious consumers: vcpkg unions features across the
dependency graph and ignores default-features:false on transitive deps, so
without this the SDK's own edge forces json1 on and no consumer can opt out.

Measured: a consumer that also sets {"name":"sqlite3","default-features":false}
in its root manifest links ~52 KB smaller (SQLITE_OMIT_JSON) on
x64-windows-static Release; the vcpkg integration test stays 10/10.

- tools/ports/cpp-client-telemetry/vcpkg.json: sqlite3 default-features:false;
  port-version 1 (port-only change over the published 3.10.161.1#0)
- docs/building-with-vcpkg.md: document the required root-manifest opt-out

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(vcpkg): add consumer linker dead-strip guidance; clarify json1 opt-out

Address Copilot round comments on docs/building-with-vcpkg.md and document the
footprint lever consumers actually need (validated end-to-end on a real
downstream DLL, which shrank substantially once /OPT:REF,ICF were on).

- Add "Enable linker dead-stripping" section: the SDK's /Gy /Gw only enable
  stripping; the consumer's link must set /OPT:REF + /OPT:ICF (with the /DEBUG
  gotcha that silently disables them) + /INCREMENTAL:NO, or --gc-sections /
  -dead_strip on GNU/Clang/Apple. Note static-link vs DLL-reexport caveat.
- json1 section: clarify this is the in-repo OVERLAY port (registry port to
  follow upstream), addressing the "registry still pulls defaults" comment.
- Reword the resolution explanation around vcpkg's union model instead of
  "ignores transitive default-features:false". Verified by dry-run: SDK edge
  opt-out alone keeps sqlite3[core,json1]; adding the same at the root yields
  sqlite3 (no json1); any edge requesting defaults restores json1 -- so the
  consumer must opt out at the root too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* vcpkg(overlay): drop port-version

The in-repo overlay is only used for local testing (overlays ignore the
version database), and the vcpkg-release-bump workflow deletes port-version
on every bump, so it served no purpose here.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(vcpkg): precise /DEBUG wording for /OPT:REF,ICF

Address Copilot comment on the linker-stripping guidance. Per the MSVC /OPT
docs, /DEBUG changes the /OPT default from REF/ICF to NOREF/NOICF (it does
disable them by default, contrary to the comment's premise). Reword to the
exact behavior ("flips their default to off, /OPT:NOREF,NOICF") and note that
/OPT:REF is also incompatible with incremental linking (hence /INCREMENTAL:NO).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* cmake: clarify AppleClang dead-strip comment (ld64 atomizes per symbol)

Address Copilot comment: reword the AppleClang branch to state the actual
mechanism -- clang emits .subsections_via_symbols on Mach-O, so ld64's
-dead_strip removes unreferenced code at per-symbol (function) granularity
without -ffunction-sections (which we add only for cross-toolchain consistency).
-fdata-sections stays omitted due to the historical bitcode conflict. No
behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build: hide non-public symbols on non-Windows (-fvisibility=hidden)

Add -fvisibility=hidden -fvisibility-inlines-hidden for non-MSVC builds and make
MATSDK_LIBABI = __attribute__((visibility("default"))) on GCC/Clang, so only the
MATSDK_LIBABI-decorated public API (the 56 public classes + the EVTSDK_LIBABI C
API in mat.h) is exported; SDK internals and the bundled sqlite3/zlib are hidden.

Non-Windows analog of the __declspec(dllexport)-gated export on Windows and of
/Gy + the consumer's /OPT:REF: a much smaller dynamic symbol table -> faster
dynamic linking/loading, smaller shared binaries, and more inlining/dead-code
elimination. No behavior change for static consumers (hidden symbols remain
usable within the same link); for shared-lib builds it restricts exports to the
public API.

Validated (NDK aarch64): compiling EventProperties.cpp with the flag yields the
decorated public methods (EventProperties::SetType/GetType) as GLOBAL DEFAULT
while internals (EventPropertiesStorage) are WEAK HIDDEN -- 64 exported vs 300
hidden in that one TU. Full cross-platform validation (build a shared lib and
link a separate consumer on Linux/macOS/iOS/Android to confirm no public symbol
is missing) should run in CI before merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address Copilot round on microsoft#1475: clang-cl /Gw gating + workflow_dispatch input

- CMakeLists.txt: if(MSVC) is also true for the ClangCL toolset, which supports
  /Gy but not /Gw. Apply /Gy for all MSVC-like compilers and gate /Gw to real
  cl.exe (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") so ClangCL builds don't choke
  on /Gw.
- vcpkg-release-bump.yml: read the workflow_dispatch tag via
  github.event.inputs.tag (concurrency key + resolve step) so manual dispatches
  resolve the tag unambiguously.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Windows shared-lib: add a dllimport path and tie export decoration to linkage

The SDK has no .def file, so __declspec(dllexport)/(dllimport) via MATSDK_LIBABI
is the sole Windows export mechanism, but it only had export and static states --
no dllimport path for a consumer of a shared mat.dll. And the cmake build
hard-defined MATSDK_SHARED_LIB=1 for every Win32 build regardless of
BUILD_SHARED_LIBS, so a static build still decorated the public API with
dllexport (which also re-exports SDK symbols from any consumer DLL that absorbs
the static lib).

- ctmacros.hpp: add a MATSDK_IMPORT_LIB branch -> __declspec(dllimport).
- lib/CMakeLists.txt: drop the hard-coded MATSDK_SHARED_LIB=1; instead set it on
  the mat target by linkage. Shared: PRIVATE MATSDK_SHARED_LIB (export from the
  SDK) + INTERFACE MATSDK_IMPORT_LIB (carried by the installed MSTelemetry::mat
  target, so find_package() consumers get dllimport automatically). Static:
  MATSDK_STATIC_LIB so nothing is decorated.

This makes the C++ public API safe to consume from a single shared mat.dll (and
fixes the missing-dllimport gap). The MSBuild/.vcxproj projects are unaffected
(they define MATSDK_SHARED_LIB themselves).

Add docs/sharing-a-single-sdk-runtime.md: how to ship one shared mat runtime that
multiple modules in a process import (stable C ABI recommended; C++ shared-DLL
path with its ABI-matching requirements), per-port vcpkg linkage, single
LogManager lifetime ownership, one-copy-on-the-loader-path, and validation.

Validated: MSVC preprocessor expands MATSDK_LIBABI to dllexport / dllimport /
empty for the shared / import / static cases; cmake configures and the static
Linux build is unaffected (change is Windows-guarded). Windows shared
export/import is exercised by CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Scope -fvisibility-inlines-hidden to C++ to avoid -Werror C failures

-fvisibility-inlines-hidden is a C++-only option. Applying it to all
languages via add_compile_options meant the legacy Android path's bundled
C sources (sqlite3_bundled, zlib_bundled) received it too; under Clang this
emits an unused-argument warning that becomes an error with the project's
-Werror. Scope it to CXX via a COMPILE_LANGUAGE generator expression while
keeping -fvisibility=hidden for both C and C++.

Files changed:
- CMakeLists.txt: -fvisibility-inlines-hidden gated to \$<COMPILE_LANGUAGE:CXX>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Re-export ObjC wrapper classes in shared Apple builds

The global -fvisibility=hidden (root CMakeLists.txt) hides Objective-C
class symbols (_OBJC_CLASS_\*) as well as C++ internals. With the
default BUILD_OBJC_WRAPPER=YES, a shared libmat.dylib on Apple would
therefore export none of the public ODW* wrapper classes, and consumers
linking against them would fail with undefined _OBJC_CLASS_\... symbols.

Collect the ObjC wrapper translation units into OBJC_WRAPPER_SRCS and, for
shared Apple builds, compile just those units with -fvisibility=default so
the public Objective-C API is re-exported while the C++ core stays hidden.

Files changed:
- lib/CMakeLists.txt: OBJC_WRAPPER_SRCS variable + per-source -fvisibility=default for shared Apple builds

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use portable OR generator expression in footprint docs snippet

The multi-config form \$<CONFIG:Release,RelWithDebInfo> only matches on
CMake >= 3.19; on older CMake it compares the literal string and never
matches, so a consumer copy-pasting the snippet would silently fail to
enable /OPT:REF, /OPT:ICF, and /INCREMENTAL:NO. Switch to
\$<OR:\$<CONFIG:Release>,\$<CONFIG:RelWithDebInfo>>, which is valid across
all supported CMake versions.

Files changed:
- docs/building-with-vcpkg.md: OR-based CONFIG generator expression in the dead-strip snippet

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Gate visibility on NOT WIN32; clarify C-API dllimport doc wording

Two review fixes:

- CMakeLists.txt: the hidden-visibility block is described as non-Windows
  but was gated on NOT MSVC, which also matches MinGW/Clang-GNU Windows and
  would apply ELF-style -fvisibility=hidden to a PE/COFF target. Gate it on
  NOT WIN32 so all Windows toolchains rely on __declspec(dllexport) as
  intended.
- docs/sharing-a-single-sdk-runtime.md: the C-API bullet said it 'links
  without __declspec(dllimport)', which contradicted the new MATSDK_IMPORT_LIB
  interface define. Reworded to: dllimport is not required (a C function
  resolves via the import-lib thunk) but is applied automatically to shared
  consumers as a harmless optimization.

Files changed:
- CMakeLists.txt: NOT MSVC -> NOT WIN32 for the visibility block
- docs/sharing-a-single-sdk-runtime.md: C-API dllimport wording

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Scope footprint guidance to static-link scenarios

The 'Reducing binary footprint' section assumed the SDK is always linked
statically, but vcpkg's default triplets (e.g. x64-windows) build dynamic
libraries and the port also supports BUILD_SHARED_LIBS=ON. Clarify that the
consumer-side dead-stripping guidance applies to static linkage, and note
that a dynamic mat ships its own runtime whose export table is already
trimmed by the SDK's -fvisibility=hidden and /Gy /Gw.

Files changed:
- docs/building-with-vcpkg.md: scope footprint section to static-link case

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Gate non-Windows default-visibility on shared builds (mirror Windows)

On non-Windows, MATSDK_LIBABI unconditionally expanded to
__attribute__((visibility("default"))), so even in a static build the
public API kept default visibility despite -fvisibility=hidden. A consumer
that statically absorbs libmat.a into its own .so/.dylib would then
unintentionally re-export the SDK's public API (larger dynamic symbol
table, leaked SDK surface) -- the non-Windows analog of the Windows
re-export that MATSDK_STATIC_LIB already prevents.

Gate the visibility attribute on MATSDK_SHARED_LIB so it mirrors the
__declspec(dllexport) gating: shared builds export the API; static builds
omit the attribute, letting the public symbols inherit -fvisibility=hidden.
Define MATSDK_SHARED_LIB PRIVATE for all shared builds in lib/CMakeLists.txt
(keeping the Windows-only INTERFACE MATSDK_IMPORT_LIB and MATSDK_STATIC_LIB).

Verified on Linux (readelf): static build -> evt_api_call_default is
GLOBAL HIDDEN (still resolvable by static linking -- a consumer links+runs
against libmat.a -- but not re-exported); shared build -> GLOBAL DEFAULT
(exported from libmat.so).

Files changed:
- lib/include/public/ctmacros.hpp: non-Windows MATSDK_LIBABI gated on MATSDK_SHARED_LIB
- lib/CMakeLists.txt: define MATSDK_SHARED_LIB for all shared builds

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Prepare for new release - 3.10.170.1

Regenerated lib/include/public/Version.hpp via tools/gen-version
(date-derived version: 3.<(year-2020)+4>.<dayOfYear>.1 = 3.10.170.1).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update version to 3.10.173.1

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ropagation (microsoft#981), and docs (microsoft#1458)

* Document supported Windows build toolsets

Clarify the VS2019/VS2022/VS2026 command-line build entry points, avoid legacy .NET Framework 4.0 projects in VS2022+ wrappers, and make solution-level :Build targets work through RunMsBuild.

Files changed:
- docs/cpp-start-windows.md
- build-all.bat
- build-all-v143.bat
- build-all-v145.bat
- tools/RunMsBuild.bat
- tools/setup-buildtools.cmd
- tools/vcvars.cmd
- tools/.vsconfig.vs2022
- tools/.vsconfig.vs2026
- Solutions/before.targets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Windows build script review comments

Clarify the Windows Visual Studio build entry point by moving the build matrix implementation to build-all-windows.bat while keeping build-all.bat as a compatibility wrapper. Update version-specific wrappers, CI, helper scripts, and docs to call the clearer name.

Also clarify that MFC/ATL Visual Studio components remain intentional because SampleCppMini uses static MFC.

Validation:
- git diff --check
- build-all-v142.bat Solutions\build.compact-dll.props smoke with all build legs skipped confirmed custom props forwarding
- material self-review found no remaining issues

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci(windows): make the VS2022 build gate fail correctly

Addresses code-review findings on the new Windows build CI:
- build-windows-vs2022.yaml ran build-all-windows.bat with v143/vs2022 but
  without SKIP_NET40_BUILD, so it tried to build the legacy net40 /
  SampleCsNet40 projects that VS2022 cannot build (and that the docs say are
  skipped). Add SKIP_NET40_BUILD: 1 to match the canonical build-all-v143.bat.
- build-all-windows.bat called tools\RunMsBuild.bat ~12 times with no errorlevel
  check between them, so only the last build's exit code reached the caller; an
  intermediate config failure was swallowed and the gating CI job could report
  success on a broken build. Add 'if errorlevel 1 exit /b 1' after each call to
  fail fast. (Pre-existing in build-all.bat, but newly load-bearing now that a CI
  gate runs this script; verified the swallowing and the fix with an isolated
  batch repro.)
- Remove continue-on-error: true from the Checkout step so a failed checkout
  fails the job.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build(win): add concrete Windows 11 SDK 10.0.22621 to vs2022/vs2026 .vsconfig

The vs2022/vs2026 component configs only listed the bare
Microsoft.VisualStudio.Component.Windows10SDK; since setup-buildtools.cmd
applies them with --quiet and no --includeRecommended, a fresh install could
end up with no concrete Windows SDK and fail to build. Add the explicit
Windows11SDK.22621 component (the 10.0.22621 platform SDK the repo's CodeQL
workflow already pins via WindowsSDKVersion: 10.0.22621.0), mirroring how
.vsconfig.vs2019 pairs the bare component with a concrete Windows10SDK.18362.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address Copilot review: doc project list + robust cd in build-all-windows.bat

- docs/cpp-start-windows.md: drop 'win32-cs' from the list of legacy .NET 4.0
  projects the VS2022/2026 wrappers skip. MSTelemetrySDK.sln only contains net40
  and SampleCsNet40 (gated by SKIP_NET40_BUILD); win32-cs lives in a separate
  Solutions/win32-cs solution and is not part of the SDK build matrix.
- build-all-windows.bat: use 'cd /d "%~dp0"' so the script reliably changes drive
  and tolerates spaces when invoked from another drive/working directory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address Copilot review: locate sibling scripts via %~dp0, not CWD

The VS-version wrappers (build-all-v142/v143/v145.bat) called
build-all-windows.bat by bare relative name, and tools\build-Win10-compact-exp.cmd
used 'cd ..' + a CWD-relative call. Invoked from another drive/working directory
(e.g. via an absolute path), the sibling script wouldn't be found. Call it via a
%~dp0-relative absolute path instead so the wrappers work from any CWD:
- build-all-v142/v143/v145.bat: call "%~dp0build-all-windows.bat" %*
- build-Win10-compact-exp.cmd: drop the fragile 'cd ..'/%CD% and call
  "%~dp0..\build-all-windows.bat" with the props file as a %~dp0-relative path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build(win): fail with a clear message when Visual Studio is not detected

vcvars.cmd sets VSTOOLS_NOTFOUND and exits 0 when it can't find any Visual
Studio install, but RunMsBuild.bat didn't check it and went straight to
msbuild, so a developer with no/undetected VS only saw a cryptic
"'msbuild' is not recognized". Check VSTOOLS_NOTFOUND after calling vcvars and
print an actionable error (install VS with the C++ workload, or run
setup-buildtools.cmd, or set VSTOOLS_VERSION) before aborting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build(win): warn when VS detection falls back to a different version

vcvars.cmd's label cascade silently moves forward when the requested Visual
Studio isn't installed (e.g. a vs2022 request ends up on vs2026). Combined with
the version wrappers pinning PlatformToolset (v143/v145), that mismatch surfaces
later as a confusing toolset error from msbuild. Capture the explicitly
requested version and, once configuration succeeds, print a clear warning when
the detected VS differs from what was asked for. The configured path now returns
a deterministic exit 0 (callers key off VSTOOLS_NOTFOUND, not the exit code).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build(win): make VSTOOLS_NOTFOUND robust to stale shell state

Copilot round: vcvars.cmd set VSTOOLS_NOTFOUND=1 on the not-found path but
never cleared it on success, so a stale value left in the shell by a previous
failed run could make RunMsBuild.bat's new guard abort a build even when VS is
present. Clear VSTOOLS_NOTFOUND at vcvars.cmd entry (only the not-found path
sets it now) and check the explicit ==1 value in RunMsBuild.bat instead of mere
existence. Verified at tools/vcvars.cmd entry and tools/RunMsBuild.bat:17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build(win): clear stale VS output vars at vcvars.cmd entry

Copilot round: clearing only VSTOOLS_NOTFOUND wasn't enough. Callers such as
tools\setup-buildtools.cmd gate on if exist "%VSINSTALLDIR%" and use
%VSVERSION% for the .vsconfig path, so a stale VSINSTALLDIR/VSVERSION/VSDEVCMD
left in the shell (or caller environment) could make them act on the wrong
install after a failed detection. Reset VSINSTALLDIR, VSDEVCMD and VSVERSION
alongside VSTOOLS_NOTFOUND at entry so each run starts from a fully clean
detection state; only the matching detection path repopulates them.

Verified: stale VSINSTALLDIR is replaced with the real path on success and left
empty when detection doesn't match (tools/setup-buildtools.cmd:42,44).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ionData parse hardening) (microsoft#1179)

* Add noexcept on non throwing methods identified by static analysis

* LogSessionDataProvider: harden convertStrToLong (reset errno, reject negatives)

Addresses the Copilot low-confidence (suppressed) review note on
convertStrToLong:
- strtoll's errno was checked without being reset first, so a stale errno from
  an earlier call could trip false "conversion failed" handling. errno is now
  cleared before the call.
- a negative input wrapped silently into a large uint64_t. The value is now
  parsed into a signed temp and rejected (returns 0 + warns) if negative.
- the imprecise res==LONG_MAX overflow heuristic is replaced by a direct
  errno==ERANGE check plus an explicit no-conversion / trailing-character check.

noexcept is preserved (no throwing operations). Pre-existing logic, folded in
here since this PR already annotates the same function.

Verified: NDK aarch64-linux-android23 -fsyntax-only clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* LogSessionDataProvider: parse with strtoull to match uint64_t return type

Follow-up to the Copilot review on convertStrToLong: the function returns
uint64_t but parsed via std::strtoll (signed long long), which constrains the
accepted range to LLONG_MAX and mixes signed/unsigned. Switch to std::strtoull
so parsing matches the return type and the full uint64_t range is accepted.
strtoull silently wraps a leading '-', so negatives are now rejected explicitly
(first non-space char check) before parsing, preserving the earlier
negative-rejection behavior. noexcept preserved.

Verified: NDK aarch64-linux-android23 -fsyntax-only clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* LogSessionDataProvider: mark remove_eol noexcept

remove_eol only inspects and shrinks the string in place (empty(), operator[],
length(), and erase() at a validated position) -- none of which allocate or
throw -- so it is genuinely non-throwing. Extends this PR's noexcept coverage to
the one remaining sibling helper in the session-data classes that is safely
non-throwing (parse()/writeFileContents()/the std::string ctor allocate, so they
correctly stay un-annotated). Verified NDK aarch64-linux-android23 -fsyntax-only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* EventProperties: add noexcept move constructor and move assignment

EventProperties is pimpl (single EventPropertiesStorage* m_storage) with a
user-declared copy ctor/assign + virtual dtor, so the compiler generated no
move operations -- every pass/return/vector-growth deep-copied the whole
property map via `new EventPropertiesStorage(*copy.m_storage)`. Add O(1)
noexcept move ctor + move assignment that transfer the storage pointer.

The dtor is already null-safe (delete nullptr); copy-assignment is now also
null-safe so a moved-from object can be reassigned. Backward-compatible API
addition (the single-pointer layout is unchanged).

Verified: NDK aarch64-linux-android23 -fsyntax-only clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* LogSessionDataProvider: fix %d format specifier for const char* names

The three failure-path LOG_WARN calls passed the static const char*
setting names (sessionFirstLaunchTimeName/sessionSdkUidName) to a %d
conversion, which is undefined behavior and logs a garbage integer
instead of the setting name exactly when a store/delete failed. Line 71
already uses %s with .c_str(); make these three consistent with %s.

Folded into this PR since it already hardens this file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Lalit Kumar Bhasin <labhas@microsoft.com>
Co-authored-by: Bhagirath Mehta <bhamehta@microsoft.com>
Co-authored-by: bmehta001 <bmehta001@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…f copying (microsoft#1498)

* Decorator: move CsProtocol::Value temporaries into the ext maps

EventPropertiesDecorator builds a throwaway CsProtocol::Value (or the Part B
map) for every event property and copy-assigned it into the ext/extPartB maps.
Each value is a local that is not used after insertion, so move it instead of
copying. CsProtocol::Value is a heavy type (vectors of attributes/PII plus
strings), and this runs on the per-event decorate path.

Pure move-instead-of-copy of throwaway locals; no interface or behavior change.
EventProperties decorator/serialization tests (54) pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot comment: include <utility> for std::move

The decorator now uses std::move; add the explicit <utility> include instead of
relying on transitive includes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…icrosoft#1484)

OfflineStorageTests_SQLite.InitializeDeletesFileAndCreatesNewIfFailed fails
only on iOS: after a corrupt DB is detected, recreate() opens with
deletePrevious=true, but the open then fails and OnStorageOpened reports
"SQLite/None" instead of the expected "SQLite/Clean".

Root cause hypothesis: deletePrevious only removed the main database file via
the SQLite VFS xDelete. A stale -journal/-wal/-shm companion left behind by
the failed first open can prevent the freshly created database from opening
cleanly. This is benign on Windows/Linux (where the test passes) but trips the
iOS VFS.

Fix: in SQLiteWrapper::initialize's deletePrevious path, delete the main DB
file plus its -journal/-wal/-shm companions. Only a failure to delete the main
file is fatal; a leftover companion that cannot be removed no longer aborts the
recreate, since the subsequent open may still succeed.

No desktop regression: built UnitTests (Release x64, v145) and ran the full
OfflineStorageTests_SQLite suite -- 43/43 pass on Windows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…icrosoft#1483)

* android: initialize EventPropertiesStorage.eventType to fix null getType()

EventPropertiesStorage's default constructor initialized every member
except eventType, so EventProperties.getType() returned null on a freshly
constructed object instead of its documented empty-string default (microsoft#1329).
On Android/Java this surfaced as a NullPointerException for callers.

- EventPropertiesStorage(): initialize eventType = "" (matching eventName
  and the other members).
- EventsUnitTest: add newEventPropertiesGetTypeReturnsEmptyString as a
  regression test (getType() on a new EventProperties is non-null and "").

Resolves microsoft#1329.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Don't forward an empty event type to native SetType (Android)

Code review of the microsoft#1329 fix found a native-path regression. Making Java
getType() return "" (instead of null) fixes the Java-side NPE, but the JNI
converter forwarded the type to native whenever the jstring was non-null:

    if (jstrEventType != NULL)
        eventProperties.SetType(JStringToStdString(env, jstrEventType));

Before microsoft#1329, getType() returned null for a default EventProperties, so
SetType was skipped and native eventType stayed at its "" default. Now the
non-null "" reaches native SetType(""), which fails validateEventName (length
< 4) and, for EVERY typeless event, logs "Invalid event type!" and broadcasts
an EVT_REJECTED DebugEvent to all registered listeners (EventProperties.cpp
SetType -> ILManager::DispatchEventBroadcast) -- a false-positive rejection
signal indistinguishable from a genuinely rejected event.

Fix: in the JNI converter (the sole native SetType chokepoint), treat an empty
type as "unset" and only call SetType for a non-empty string, restoring the
pre-microsoft#1329 native behavior while keeping the corrected Java API contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot on microsoft#1483: test EventPropertiesStorage directly (no native)

The regression test constructed EventProperties, whose constructor calls
setName() -> native Utils.validateEventName(). These are JVM unit tests
(@RunWith(MockitoJUnitRunner)) with no native library loaded, so it would throw
UnsatisfiedLinkError instead of exercising the regression. Test the pure-Java
EventPropertiesStorage (same package) directly -- the exact class the microsoft#1329 fix
initialized (eventType = "").

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rosoft#1480)

* Build native desktop SDK in MSVC conformance mode (/permissive-)

Enable ConformanceMode (/permissive-) for the native desktop SDK
projects via the shared Solutions/build.props, so non-standard MSVC
extensions are caught at build time (issue microsoft#255).

Two project families are deliberately excluded, gated in the
ItemDefinitionGroup condition:
  * UWP / Windows Store projects (AppContainerApplication=true) compile
    as C++/CX (/ZW), which MSVC rejects in combination with /permissive-.
  * C++/CLI managed projects (Keyword=ManagedCProj, e.g. net40) target
    the managed runtime and are a separate conformance domain.

build.props is imported only by the SDK projects (net40, win10-*,
win32-*), not by vendored sqlite/zlib or the test projects, so vendored
third-party code is unaffected.

Validated locally (VS 2026, v145, x64 Release): clean Rebuild of
win32-lib (58 files) plus win32-dll / win32-mini-lib / win32-mini-dll
all compile with 0 conformance errors and 0 warnings. Confirmed the gate
excludes UWP (win10-lib does not receive /permissive-, so its /ZW build
is unaffected).

Files: Solutions/build.props

Resolves microsoft#255.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Extend conformance mode to net40 (C++/CLI) and fix /permissive- errors

Bring the net40 managed (C++/CLI) project under /permissive- by dropping
the Keyword=ManagedCProj exclusion from the build.props gate, and fix the
two conformance errors this surfaced in shared CX code:

  * lib/shared/EventPropertiesCX.cpp: add `typename` to the dependent
    name `map<string,T>::iterator` in StoreEventProperties (C3878/C2065
    under two-phase name lookup).
  * lib/shared/PlatformHelpers.h: forward-declare FromPlatformString
    before the FromPlatformMap templates that call it. Under strict
    two-phase lookup the dependent call fell back to ADL (which searches
    Platform::, not the SDK namespace) and failed with C3861.

Now only UWP / Windows Store (C++/CX, /ZW) projects are excluded, since
/permissive- is incompatible with /ZW.

Validated (VS 2026, v145, x64 Release): clean Rebuild of the full
non-UWP SDK set -- win32-lib, win32-dll, win32-mini-lib, win32-mini-dll
and net40 -- with 0 conformance errors. (net40 is .NET Framework 4.0
C++/CLI; local builds need the v4.0 reference-assembly targeting pack,
which the SDK CI provides.)

Files: Solutions/build.props, lib/shared/EventPropertiesCX.cpp,
lib/shared/PlatformHelpers.h

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build: address Copilot round-1 — scope /permissive- to SDK library projects only

Solutions/build.props (Copilot): the ConformanceMode block lived on the
shared build.props, which is ALSO imported by vendored sqlite/zlib, the
test projects (gtest/gmock/UnitTests/FuncTests) and the samples -- so
/permissive- was applied far more broadly than intended, and the PR
description's claim was wrong. Verified with `git grep -l build.props --
'*.vcxproj'`: sqlite/sqlite.vcxproj, third_party/.../zlibvc.vcxproj and
tests/{functests,unittests}/*.vcxproj all import it.

Fix: move ConformanceMode into a dedicated Solutions/conformance.props,
imported explicitly only by the five native SDK library projects
(win32-lib, win32-dll, win32-mini-lib, win32-mini-dll, net40), right
after their build.props import so the setting wins. build.props is
reverted to its original content.

Validated (VS 2026, v145, x64 Release): /permissive- present on
win32-lib's cl invocations (0 conformance errors); a standalone
sqlite:Rebuild now shows 0 /permissive- occurrences; clean Rebuild of
all five SDK library projects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er final + TU-local statics (microsoft#1488)

* devirt: mark concrete leaf impl classes final

Mark the leaf concrete implementations of IHttpClient and IOfflineStorage final
so the compiler can devirtualize (and often inline) calls made through them:
HttpClient_{Curl,WinInet,WinRt,Apple,Android,CAPI} and OfflineStorage_Room /
MemoryStorage / OfflineStorageHandler.

Each was verified to have no subclass anywhere in the tree (lib + tests +
wrappers). Deliberately NOT marked:
 - OfflineStorage_SQLite -- tests/unittests/OfflineStorageTests_SQLite.cpp
   subclasses it (OfflineStorage_SQLiteNoAutoCommit).
 - TelemetrySystemBase -- base of TelemetrySystem / AITelemetrySystem.

Validated: NDK aarch64 -fsyntax-only on OfflineStorage_Room, OfflineStorageHandler
and MemoryStorage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Internal linkage: mark TU-local C-API helpers and DllMain global static

Companion to the `final` devirtualization in this PR: give internal
linkage to translation-unit-local symbols so the compiler can inline /
drop them and keep them out of the (static-archive and shared-object)
symbol table. This helps the static-lib consumption path that
-fvisibility=hidden does not fully cover, since hidden visibility only
trims the dynamic export table while these symbols keep external linkage
across TUs.

lib/api/capi.cpp: mark the 10 file-local C-API dispatch helpers static
  (remove_client, mat_open_core, mat_open, mat_open_with_params, mat_log,
   mat_close, mat_pause, mat_resume, mat_upload, mat_flushAndTeardown).
  Verified each is called only from the single exported entry point
  evt_api_call_default (and each other) within capi.cpp, and appears in
  no header and no other translation unit. capi_get_client stays external
  (it is MAT::capi_get_client, declared in a header and used by the CAPI
  HTTP client). Consistent with the file's existing static mtx/clients.

lib/shared/dllmain.cpp: mark thread_count static -- a file-scope mutable
  global mutated only inside DllMain in this TU.

get_platform_uuid (sysinfo_sources.cpp) was deliberately NOT touched: it
has no caller in-tree, so marking it static would trip -Wunused-function
under -Werror.

Verified: NDK clang aarch64 -fsyntax-only on capi.cpp is clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix typo in capi.cpp comment: Marashal -> Marshal

Addresses Copilot review comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Narrow scope: keep 'final' only where it measurably devirtualizes

Per review feedback (lalitb), measured the devirtualization effect of the
'final' markers (vendored Linux shared libmat.so, -O2 -ffunction-sections
-fvisibility=hidden -Wl,--gc-sections, no LTO), base vs this PR:

- Final binary size: NO change (.text byte-size identical). Devirtualization
  swaps an indirect call for a same-width direct call -- perf, not size.
- 'final' devirtualizes only where the concrete type is known: object-level
  indirect-call counts dropped in OfflineStorageHandler.o (70->64) and
  MemoryStorage.o (8->3) -- a class calling its own virtual methods and
  OfflineStorageHandler's concrete MemoryStorage member. Call sites routed
  through IHttpClient/IOfflineStorage base references were unchanged
  (LogManagerImpl/TelemetrySystem/OfflineStorage_SQLite/HttpClient_Curl TUs
  byte-identical).

So 'final' on the HttpClient_* clients and OfflineStorage_Room bought no
measured devirtualization while still restricting subclassing. Drop it from
those; keep it only on MemoryStorage and OfflineStorageHandler (internal
lib/offline impl types, not extension points), where it does help. The
internal-linkage (static) cleanups in capi.cpp and dllmain.cpp are orthogonal
and retained.

Files: lib/http/HttpClient_{Android,Apple,CAPI,Curl,WinInet,WinRt}.hpp,
lib/offline/OfflineStorage_Room.hpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Drop redundant virtual on non-override methods in final storage classes

Newer Clang (macOS-latest, LLVM 19+) enables -Wunnecessary-virtual-specifier,
which errors under -Werror when a 'virtual' method that does not override a base
method lives inside a 'final' class (it can never be overridden). Marking
OfflineStorageHandler and MemoryStorage final left three such methods
(DeleteRecordsByKeys, isKilled, GetReservedCount) still declared virtual,
breaking the macOS debug build. Remove the now-redundant virtual specifier;
these become non-virtual, consistent with the devirtualization goal. No
subclasses or overrides of these methods exist.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…microsoft#1500)

* Skip MultipleLogManagersTests on iOS (hangs the simulator CI job)

The iOS CI build (build-ios-mac.yml -> iOSFuncTests) consistently sat until its
60-minute timeout. The hang point is MultipleLogManagersTests.ThreeInstancesCoexist:
these tests stand up an in-process HttpServer on a loopback port and run multiple
concurrent LogManager instances uploading to it, which deadlocks inside the iOS
simulator sandbox (the log shows the test starting, a loopback 'Connection reset
by peer', then no further output until the job is canceled at 60 minutes).

Skip the whole fixture on iOS via GTEST_SKIP() in SetUp(), guarded by
TARGET_OS_IPHONE so macOS and desktop targets keep exercising it. The guard is a
no-op on non-Apple platforms (TARGET_OS_IPHONE undefined).

Files: tests/functests/MultipleLogManagersTests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* iOS: exclude MultipleLogManagersTests at compile time, not via GTEST_SKIP

The first attempt skipped the suite via GTEST_SKIP() in SetUp(), which stopped
the 60-minute hang but the iOS xctest gtest wrapper does not honor a SetUp skip:
the test bodies still ran (with the HttpServer never started) and failed at
MultipleLogManagersTests.cpp:183 and :221.

Exclude the whole suite from the iOS build with #if !defined(TARGET_OS_IPHONE) ||
!TARGET_OS_IPHONE so the tests don't exist in the iOS binary at all. macOS,
Linux and Windows still build and run them (guard is true there; on non-Apple
TARGET_OS_IPHONE is undefined -> included).

Files: tests/functests/MultipleLogManagersTests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… logs (microsoft#1493)

* Low-severity hardening: CSPRNG UUIDs (POSIX) + null-safe zlib error logs

Two low-severity issues found during a repo-wide review:

1) PAL::generateUuidString POSIX/Android fallback built the UUID entirely
   from std::rand(), seeded once with srand(time(0) ^ nanos). std::rand()
   is a weak, predictable PRNG with a guessable time-based seed, so the
   session / event / instance identifiers derived from it were
   predictable. Source the bytes from std::random_device instead (backed
   by /dev/urandom on Linux/Android), matching the existing
   CorrelationVector.cpp / PseudoRandomGenerator usage. Windows
   (CoCreateGuid) and Apple (CFUUIDCreate) paths are unchanged.

   Test: PalTests.UuidGeneration extended to assert 1000 generated UUIDs
   are all distinct (in addition to the existing format/entropy checks).

2) zlib error-path logs passed stream.msg / zs.msg straight to a %s
   conversion. zlib leaves msg == Z_NULL for several error codes
   (Z_MEM_ERROR, Z_BUF_ERROR, after a failed deflateInit2), and
   printf("%s", NULL) is undefined behavior -- benign "(null)" on glibc
   but not guaranteed across the MSVC/Android/Apple CRTs this SDK targets.
   Guard with `msg ? msg : "(null)"` in ZlibUtils.cpp and the two
   HttpDeflateCompression.cpp sites.

Verified on Linux host: UnitTests builds; PalTests (incl. the UUID
uniqueness check) and ZlibUtilsTests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review comment: avoid reopening entropy source per UUID

lib/pal/PAL.cpp (generateUuidString, POSIX/Android path): std::random_device was
default-constructed on every call, which reopens the entropy source
(/dev/urandom) per UUID on the event-logging hot path. Mark it thread_local so
it is opened once per thread and reused; each operator() still draws fresh
CSPRNG bytes, so the unpredictability property is unchanged and per-thread
isolation keeps it lock-free.

Verified the distinctness guard (PalTests.UuidGeneration, 1000 unique UUIDs)
still passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot round-1 comments

zlib error logs (ZlibUtils.cpp:51, HttpDeflateCompression.cpp:47,83): zlib
return codes are signed and frequently negative (Z_DATA_ERROR=-3, etc.).
Logging them with %u misrepresented the value and was a format/type mismatch;
use %d for both the step and the code.

PAL.cpp generateUuidString (POSIX/Android): reduce std::random_device reads from
11 to 4 (random_device::max() spans the full unsigned int range, so 4x32 bits
fills the 128-bit GUID), cutting per-event-ID entropy-source reads on the hot
path. Also soften the comment: random_device is non-deterministic/CSPRNG-backed
on our target platforms but the standard does not guarantee the backing source
universally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Name UUID test magic numbers as constexpr constants

Address review feedback on PalTests UuidGeneration: replace the repeated
literals 36 (canonical UUID string length) and 1000 (uniqueness-check batch
size) with named constexpr constants UuidStringLength and UuidBatchSize so the
test's intent is clear at each use site.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Unify PalTests local constants on constexpr

Convert the remaining size_t const constants (NumQueries, NumBuckets) in the
PseudoRandomGenerator test to constexpr so all local constants in PalTests.cpp
use the same form. NumBuckets is used as an array bound, so constexpr also
documents that it must be a compile-time constant. No behavior or codegen
change (const-integral literals already fold to immediates).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ft#1495)

* Harden runtime task execution and offline-storage edge cases

These are latent correctness/robustness fixes found during a repo-wide review.

WorkerThread.cpp: wrap the queued-task invocation `(*item)()` in try/catch.
A task runs arbitrary work (storage I/O, HTTP encode, user DebugEventListener
callbacks); an exception escaping the loop unwinds out of the thread entry
function and calls std::terminate, killing the host process. Contain it and log.

TaskDispatcher_CAPI.cpp: same exception barrier around `(*m_task)()` in
Task_CAPI::OnCallback(), which runs on the host's external dispatcher thread.

capi.cpp (mat_open_core): on the EALREADY path (a guest instance with the same
config is already open) set ctx->handle to the existing instance's handle before
returning, so the caller is not left with an uninitialized/stale handle.

OfflineStorageHandler.cpp (Flush): null-guard m_offlineStorageMemory->GetSize()
read; the subsequent block already null-checks the pointer, so reading it first
was inconsistent and a potential null deref.

OfflineStorageHandler (StoreRecord): the per-instance RAM cache size limit was
held in a function-local `static`, so the first LogManager's CFG_INT_RAM_QUEUE_SIZE
leaked to every other LogManager instance. Compute it once per instance in
Initialize() into a member (preserving the original "compute once" intent).

Tests:
- PalTests.WorkerThreadContainsThrowingTask: a task that throws std/non-std
  exceptions does not tear down the worker thread; follow-up tasks still run.
- TaskDispatcherCAPITests.ExecuteCallbackThatThrowsIsContained: a throwing CAPI
  task callback does not propagate back into the host dispatcher thread.

Files changed:
- lib/pal/WorkerThread.cpp
- lib/pal/TaskDispatcher_CAPI.cpp
- lib/api/capi.cpp
- lib/offline/OfflineStorageHandler.cpp
- lib/offline/OfflineStorageHandler.hpp
- tests/unittests/PalTests.cpp
- tests/unittests/TaskDispatcherCAPITests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot round-1 comments

capi.cpp (mat_open_core, EALREADY path): also set ctx->result to match the
returned status. The success path sets ctx->result before returning; the
EALREADY early-return set ctx->handle but left ctx->result stale, inconsistent
with the other entrypoints.

TaskDispatcher_CAPI.cpp (Task_CAPI::OnCallback): log the contained exception
instead of swallowing it silently, mirroring WorkerThread, so host apps/SDK logs
can diagnose why a queued task failed. Added <exception> include for
std::exception.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot round-2 comments

WorkerThread.cpp: add explicit #include <exception>; the round-1 catch block
uses std::exception but the TU only relied on transitive includes.

capi.cpp (mat_open_core): the early error returns (invalid config, and the two
HttpClient/TaskDispatcher creation catch blocks) left ctx->result and ctx->handle
untouched. Since the public mat.h inline helpers return ctx.handle after calling
in, callers could observe a stale/uninitialized handle on error. Set
ctx->result = EFAULT and ctx->handle = 0 on all three paths (ctx is guaranteed
non-null by mat_open / mat_open_with_params).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot round-3 comments

capi.cpp (mat_open_core): if HttpClient_CAPI or TaskDispatcher_CAPI construction
threw, clients[code] had already been populated (config + ctx_data assigned), but
the catch returned EFAULT without removing it. That left an orphaned, half-
initialized entry in the global clients map, so a later open with the same config
would match ctx_data and wrongly return EALREADY. Call remove_client(code) on
both creation-failure paths before returning EFAULT.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ft#1501)

* Make public headers safe for consumers building with -Werror

Consumers that embed the SDK and compile with -Wall -Wextra -Werror (e.g. ONNX
Runtime / Foundry Local via add_subdirectory) were broken by warnings emitted
from inside the SDK's public headers. Two complementary changes fix this:

Primary (covers every consumer + every warning flag): mark mat's exported public
include directory as SYSTEM. find_package consumers already treat an imported
target's includes as system; SYSTEM extends that to add_subdirectory/FetchContent
consumers, so their -Werror no longer promotes SDK-header warnings to errors.
Verified: an -isystem consumer compiles clean under -Wall -Wextra -Wpedantic
-Wshadow -Wconversion -Werror on gcc and clang.

Defense in depth (also helps non-CMake consumers and NO_SYSTEM_FROM_IMPORTED):
- UNREFERENCED_PARAMETER(...) expanded to nothing on gcc/clang, leaving the
  parameter unused; it now casts to void, eliminating 5 -Wunused-parameter
  warnings across NullObjects.hpp and LogManagerProvider.hpp.
- assert(!"msg") triggered -Wstring-conversion on clang; switched to the
  canonical assert(false && "msg") in ISemanticContext.hpp (2 sites).

Result: including the main public headers under -Wall -Wextra now yields 0
warnings (was 5). SDK's own top-level build verified unaffected (75
UNREFERENCED_PARAMETER call sites compile clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Scope SYSTEM to the public include dir only

Address Copilot review comment on lib/CMakeLists.txt:358. The SYSTEM keyword is
a per-call flag, so a single target_include_directories(mat SYSTEM PUBLIC ...
PRIVATE ...) also marked the SDK-internal PRIVATE include dirs as system for
mat's own build, which would suppress warnings in the SDK's own internal headers
under -Werror. Split into two calls: SYSTEM PUBLIC for the exported public dir,
and a separate non-SYSTEM PRIVATE call for internal dirs. Verified via CMake
property query: INTERFACE_SYSTEM_INCLUDE_DIRECTORIES contains only the public
dir; the private internal dirs return to their original (non-system) treatment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…roject (microsoft#1499)

* Add BUILD_CURL_HTTP_CLIENT option to build Linux without curl/TLS

On the CPP11/curl path (non-Apple, non-Windows), the built-in libcurl
HTTP client was always compiled and curl was a hard find_package(CURL
REQUIRED) dependency -- pulling in curl and a TLS backend (OpenSSL/mbedTLS)
even for hosts that already have their own HTTP stack.

Add option(BUILD_CURL_HTTP_CLIENT ON). When OFF, the curl block is skipped
(no find_package(CURL), no link, no -DHAVE_MAT_CURL_HTTP_CLIENT) and the
build instead defines -DMATSDK_NO_DEFAULT_HTTP_CLIENT. mat/config.h then
undefines HAVE_MAT_DEFAULT_HTTP_CLIENT centrally (regardless of the config
preset), which the SDK already handles end-to-end: HttpClientFactory and
HttpClient_Curl.cpp compile out, and LogManagerImpl's existing
!HAVE_MAT_DEFAULT_HTTP_CLIENT branch requires the host to supply an
IHttpClient via CFG_MODULE_HTTP_CLIENT.

Default ON keeps existing behavior unchanged. Apple/Windows are unaffected
(they use native HTTP stacks and never enter the curl block).

Validated on WSL x64-linux: with OFF, libmat has no curl symbols and a
consumer links with no -lcurl/-lTLS (1.43 MB stripped, vs 4.39 MB with
curl+mbedTLS and 10.65 MB with curl+OpenSSL).

Files changed:
- CMakeLists.txt: BUILD_CURL_HTTP_CLIENT option + gating
- lib/include/mat/config.h: central HAVE_MAT_DEFAULT_HTTP_CLIENT opt-out

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add MATSDK_MINIMAL_SQLITE: private feature-stripped SQLite to cut footprint

The SDK uses SQLite only for its offline event-storage cache (plain tables,
transactions, WAL, autovacuum/VACUUM, a few PRAGMAs, and one custom UTF-8
function), so most SQLite subsystems are dead weight. Add an option to compile a
private SQLite from the vendored amalgamation with a set of amalgamation-safe
strip flags (single source of truth: MATSDK_SQLITE_MINIMAL_DEFS), removing the
external sqlite3 dependency and shrinking SQLite ~10.2% (.text) / ~12.5% (object).

- Root CMakeLists.txt: add option(MATSDK_MINIMAL_SQLITE) (default OFF). In vcpkg
  mode, skip find_package(unofficial-sqlite3) when bundling, and emit a clear
  FATAL_ERROR pointing at the system-sqlite/minimal-sqlite features when neither
  provides SQLite (e.g. a bare [core] install).
- lib/CMakeLists.txt: define MATSDK_SQLITE_MINIMAL_DEFS, compute MATSDK_BUNDLE_SQLITE
  (minimal OR vendored-Android), and build a single sqlite3_bundled. The strip
  flags are applied ONLY when MATSDK_MINIMAL_SQLITE is ON, so the default Android
  legacy build keeps its existing unstripped bundled SQLite. Warnings are disabled
  on the vendored target (/w on MSVC, -w on GCC/Clang for the stripped build) so
  the SDK's -Werror/-WX does not fire on amalgamation code. A static mat propagates
  the PRIVATE sqlite3_bundled through its link interface, so export+install it.
- MSTelemetryConfig.cmake.in: skip find_dependency(unofficial-sqlite3) when bundled.
- vcpkg port: add a minimal-sqlite feature (-DMATSDK_MINIMAL_SQLITE=ON) and move
  sqlite3 into a default system-sqlite feature so [core,minimal-sqlite] drops it.
- docs/building-with-vcpkg.md: document the feature, the size win, and the
  static-absorption symbol-visibility caveat.

SQLITE_OMIT_AUTOINIT and SQLITE_DEFAULT_MEMSTATUS=0 are deliberately NOT stripped:
the former because skipSqliteInitAndShutdown lets the host skip the SDK's explicit
sqlite3_initialize() (which the host cannot do against a private SQLite), the
latter because the SDK arms a soft heap limit via sqlite3_soft_heap_limit64() that
is only enforced while memory statistics are enabled.

Validated: vendored Linux Debug (77 offline-storage/SQLite unit tests pass on the
debug amalgamation), vcpkg [core,minimal-sqlite] consumer (links
MSTelemetry::sqlite3_bundled, runs 10/10, external sqlite3 dropped), default vcpkg
path regression (system-sqlite intact), and MSVC compile/link of sqlite3_bundled+mat.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot review: scope bundled-SQLite export to static mat

- lib/CMakeLists.txt: only add sqlite3_bundled to the install/export set when mat
  is a STATIC_LIBRARY. A shared mat absorbs the private SQLite into libmat and does
  not propagate the PRIVATE dependency, so exporting the separate archive there was
  unnecessary and could let a consumer link a second SQLite copy. For a static mat
  the archive must stay exported because the static library propagates its PRIVATE
  dependency through its link interface (\$<LINK_ONLY:...>).
- CMakeLists.txt: make the vcpkg dependency-mode status message reflect whether the
  external sqlite3 package or the private minimal SQLite is used.

Verified with an isolated CMake export test: static mat exports m+sq (consumer
linking only the namespaced lib resolves sq); shared mat exports only m and
install(EXPORT) succeeds with sq excluded. Re-ran the vcpkg [core,minimal-sqlite]
consumer (static x64-linux): 10/10.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify sqlite3_bundled PRIVATE-link comment (Copilot review)

Correct the inline comment: a PRIVATE link of the bundled SQLite suppresses
propagation of its include dirs / compile definitions, but a static mat still
propagates the archive for linking via \$<LINK_ONLY:...> (hence it is exported for
static builds); a shared mat absorbs it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* vcpkg port: selectable TLS backend (curl-openssl/curl-mbedtls) + no-default-http-client feature

Make the HTTP-client footprint a consumer choice instead of hardcoding
curl[openssl]:

- vcpkg.json: replace the base curl[openssl] dependency with three features --
  curl-openssl (default; libcurl + OpenSSL), curl-mbedtls (libcurl + mbedTLS), and
  no-default-http-client (omit the built-in client). curl-openssl is a default
  feature so a plain install keeps current behavior; [core,no-default-http-client]
  drops curl entirely.
- portfile.cmake: map the no-default-http-client feature to -DBUILD_CURL_HTTP_CLIENT=OFF
  via INVERTED_FEATURES.
- CMakeLists.txt: when the built-in client is enabled in vcpkg mode but libcurl is
  not found, emit a clear FATAL_ERROR pointing at the curl-openssl/curl-mbedtls/
  no-default-http-client features (instead of a bare find_package failure).
- docs: document the size ladder (OpenSSL ~10.6MB / mbedTLS ~4.4MB / no-curl
  ~1.4MB) and the exact mbedTLS recipe -- crucially, the consumer must ALSO list
  curl with default-features:false at the top level, because vcpkg only honors
  curl's default-features:false for top-level dependencies (otherwise curl's ssl
  default pulls OpenSSL in transitively alongside mbedTLS).

Validated on WSL with vcpkg: default resolves curl[openssl]+sqlite3; the documented
mbedTLS recipe builds with mbedTLS only (no libssl/libcrypto, libcurl carries no
OpenSSL symbols) and the consumer runs; [core,no-default-http-client] drops curl
from the graph; [core,minimal-sqlite,no-default-http-client] drops curl and the
external sqlite3.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden HTTP-client features: CURL CONFIG mode + mutual-exclusivity guard (Copilot review)

- CMakeLists.txt: in vcpkg mode use find_package(CURL CONFIG QUIET) and gate on
  TARGET CURL::libcurl. Forcing CONFIG selects the vcpkg-provided CURLConfig (which
  defines the imported target) rather than the module FindCURL, which on some CMake
  versions does not define CURL::libcurl and would fail at link.
- portfile.cmake: fail fast when more than one of curl-openssl/curl-mbedtls/
  no-default-http-client is selected. vcpkg cannot express mutual exclusivity, so a
  consumer requesting e.g. curl-mbedtls without [core] keeps the default
  curl-openssl and would union both TLS backends; the guard now errors with guidance
  to use the [core,...] form.

Validated: the guard passes single selections and fires on curl-openssl+curl-mbedtls
and curl-openssl+no-default-http-client; the default (curl-openssl) vcpkg consumer
still configures via CURL CONFIG, links, and runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove the no-curl (no-default-http-client) option

Drop the ability to build without the built-in libcurl HTTP client. That option
only benefited consumers that already ship their own IHttpClient; the SDK's named
consumers (and Apple/Windows, which use NSURLSession/WinInet) never needed it, and
it added a fragile feature plus a config-flow opt-out. The TLS-backend selection
(curl-openssl default / curl-mbedtls) and minimal-SQLite remain.

- CMakeLists.txt: remove option(BUILD_CURL_HTTP_CLIENT) and the no-curl else
  branch; the curl HTTP client is always built on the CPP11/curl path again
  (keeping the find_package(CURL CONFIG) + TARGET CURL::libcurl hardening).
- lib/include/mat/config.h: remove the MATSDK_NO_DEFAULT_HTTP_CLIENT ->
  HAVE_MAT_DEFAULT_HTTP_CLIENT opt-out.
- vcpkg.json: remove the no-default-http-client feature.
- portfile.cmake: remove the INVERTED_FEATURES mapping; the mutual-exclusivity
  guard now covers just curl-openssl vs curl-mbedtls.
- docs: drop the no-curl row/section; note the size figures are worst-case
  (without consumer-side --gc-sections).

Validated: vcpkg.json parses, CMake configures cleanly, and the mat target builds
and links with the curl client compiled in.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* vcpkg: use Apple's system libsqlite3/libz on macOS/iOS instead of vcpkg packages

macOS/iOS ship libsqlite3 and libz as system libraries, so pulling and statically
linking the vcpkg sqlite3 + zlib packages added ~1 MB of redundant code to Apple
binaries. Link the system libraries instead -- consistent with the SDK's own Swift
Package (which links .linkedLibrary("sqlite3"/"z")) and with how analogous telemetry
SDKs (e.g. sentry-native) gate these deps off Apple platforms.

- vcpkg.json: gate the zlib dependency and the system-sqlite feature's sqlite3
  dependency to "!osx & !ios" so they are not installed on Apple.
- CMakeLists.txt: on APPLE in vcpkg mode, find_package(SQLite3)/find_package(ZLIB)
  (CMake's modules resolve to the OS libraries) and set MATSDK_APPLE_SYSTEM_DEPS.
- lib/CMakeLists.txt: link SQLite::SQLite3 + ZLIB::ZLIB on Apple; never bundle a
  private SQLite on Apple (MATSDK_MINIMAL_SQLITE is a no-op there since the system
  lib is already smaller).
- MSTelemetryConfig.cmake.in: re-find system SQLite3 on Apple, the vcpkg
  unofficial-sqlite3 elsewhere.
- docs: note the Apple system-lib behavior.

Validated: non-Apple paths unchanged -- Linux vendored mat builds, and the Linux
vcpkg consumer's generated config resolves unofficial-sqlite3 (if(OFF)) and runs.
The Apple build itself needs validation on macOS/iOS CI (no Mac available here);
the risk is whether find_package(SQLite3) resolves the system lib under the vcpkg
Apple triplets' find-root settings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify mbedTLS guidance: [core,...] drops system-sqlite too

The curl-openssl/curl-mbedtls guidance in the port's fatal-error messages
and docs recommended cpp-client-telemetry[core,curl-mbedtls], but the
[core,...] form (default-features:false) drops ALL default features --
including system-sqlite -- not just curl-openssl. That example yields a
config-time failure with no SQLite backend selected.

Update both FATAL_ERROR messages (portfile.cmake mutual-exclusivity guard,
CMakeLists.txt libcurl-not-found) and the docs prose to show a complete,
working feature set ([core,curl-mbedtls,system-sqlite]) and to note that
[core,...] also drops system-sqlite, so a SQLite backend must be re-selected.

Files: tools/ports/cpp-client-telemetry/portfile.cmake, CMakeLists.txt,
docs/building-with-vcpkg.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix misleading curl 'optional' label and mbedTLS feature description

Two doc/manifest accuracy fixes from Copilot review:
- The dependency table labeled libcurl 'optional' for non-Windows/non-Apple,
  but since the no-curl option was removed, Linux/Android vcpkg builds always
  require curl (only the TLS backend is selectable). Relabel as required.
- The curl-mbedtls feature description recommended [core,curl-mbedtls], which
  drops all defaults (incl. system-sqlite); note that a SQLite backend must be
  re-selected to avoid a configure-time failure.

Files: docs/building-with-vcpkg.md, tools/ports/cpp-client-telemetry/vcpkg.json

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden [core,...] guidance and fix exported CURL find_dependency mode

Five fixes from the Copilot review on the curl/SQLite feature interactions
(all stem from vcpkg's [core,...] form dropping ALL default features, not
just one):

- portfile.cmake: fail fast on Linux/Android when no curl TLS backend is
  selected (verified: a real vcpkg install of [core,minimal-sqlite] now stops
  at the portfile with a complete [core,curl-openssl,system-sqlite] example,
  instead of a later, opaque libcurl-not-found error).
- CMakeLists.txt libcurl message: show how to re-select a curl backend (not
  only mbedTLS) under [core,...], alongside a SQLite backend.
- CMakeLists.txt SQLite message: include the valid [core,system-sqlite] path,
  not only [core,minimal-sqlite].
- MSTelemetryConfig.cmake.in: find_dependency(CURL CONFIG) so the exported
  package config uses the vcpkg CURLConfig that defines CURL::libcurl (the
  target MSTelemetryTargets references), matching the unofficial-sqlite3/
  nlohmann_json CONFIG siblings and the root CMakeLists CURL CONFIG lookup.
- docs: minimal-sqlite manifest example re-selects curl-openssl so the
  Linux/Android manifest actually configures.

Files: CMakeLists.txt, cmake/MSTelemetryConfig.cmake.in,
docs/building-with-vcpkg.md, tools/ports/cpp-client-telemetry/portfile.cmake

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Scope curl TLS-backend guards to Linux/Android only

The mutual-exclusivity check (curl-openssl vs curl-mbedtls) previously ran on
all platforms. Since curl-openssl is a default feature and the curl dependency
is platform-filtered to linux|android, a cross-platform manifest that enables
curl-mbedtls without [core] would falsely fail the port on Windows/macOS/iOS --
where curl is not used (WinInet / Apple HTTP) and neither feature pulls curl.

Wrap both the mutual-exclusivity (count>1) and no-curl (count==0) checks in a
single VCPKG_TARGET_IS_LINUX/ANDROID block so they only fire where the curl
backend selection is actually meaningful. Verified on x64-linux: [core,minimal-
sqlite] still fails with the no-curl message, and [curl-mbedtls] (no core) still
fails with the mutual-exclusivity message.

Files: tools/ports/cpp-client-telemetry/portfile.cmake

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* vcpkg port tests: build the working tree, not a pinned release

The port tests built the SDK from the portfile's pinned vcpkg_from_github REF
(v3.10.161.1), so they never exercised the PR's own source -- and the macOS/iOS
jobs failed because this PR's manifest drops the Apple sqlite3/zlib packages
while the old pinned source still calls find_package(unofficial-sqlite3)
unconditionally (the Apple system-libs branch only exists in the PR source).

Add an opt-in MATSDK_VCPKG_SOURCE_DIR hook to portfile.cmake: when set, the port
builds that local source; when unset (production installs), the pinned release is
downloaded as before, so the published port behavior is unchanged. The five
tests/vcpkg/* scripts set it to the repo root so the port tests validate the
actual source + manifest together.

Verified on Linux (x64-linux): the port now builds the working-tree SDK and the
consumer passes 10/10; the macOS/iOS jobs will exercise the Apple system-libs
branch (find_package(SQLite3)/ZLIB) instead of the dropped vcpkg packages.

Files: tools/ports/cpp-client-telemetry/portfile.cmake,
tests/vcpkg/test-vcpkg-{linux,macos,ios,android}.sh,
tests/vcpkg/test-vcpkg-windows.ps1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Windows vcpkg test to actually build the working tree

On Windows, vcpkg runs portfiles in a sanitized environment and strips custom
variables unless allow-listed via VCPKG_KEEP_ENV_VARS. Without it the portfile
never saw MATSDK_VCPKG_SOURCE_DIR and silently fell back to the pinned release
(v3.10.161.1), so the Windows port test validated the old release instead of the
PR source while still reporting PASS. Allow-list the variable so the test builds
the working tree, matching the Linux/macOS scripts (POSIX vcpkg passes the
variable through, so they need no change).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Gate test suites on top-level project (default OFF for consumers)

BUILD_UNIT_TESTS/BUILD_FUNC_TESTS defaulted to ON unconditionally, so a
downstream project consuming this repo via add_subdirectory()/FetchContent
built the whole test suite and required the third_party/googletest submodule.
Default them ON only when this repo is the top-level project
(PROJECT_IS_TOP_LEVEL on CMake >= 3.21, source-dir comparison on older CMake)
and OFF when consumed as a subproject. Direct/CI builds are unchanged
(top-level => ON) since build scripts rely on the default; verified
BUILD_UNIT_TESTS/BUILD_FUNC_TESTS=ON for a top-level configure and OFF via
add_subdirectory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot review: validate source dir, append VCPKG_KEEP_ENV_VARS

- portfile.cmake: validate MATSDK_VCPKG_SOURCE_DIR points at a real checkout
  (CMakeLists.txt present) and fail early with a clear message instead of a
  confusing downstream CMake error.
- test-vcpkg-windows.ps1: append MATSDK_VCPKG_SOURCE_DIR to VCPKG_KEEP_ENV_VARS
  instead of overwriting it, preserving any entries the caller/CI already set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Support consuming SDK as a CMake subproject in legacy mode

Applies the two changes the ONNX Runtime consumer patch carried so they can
be dropped from the downstream patch set.

Change 1 (CMakeLists.txt): use CMAKE_CURRENT_SOURCE_DIR instead of
CMAKE_SOURCE_DIR for the vendored sqlite/zlib/nlohmann include path, so the
headers still resolve when the SDK is added via add_subdirectory/FetchContent
(where CMAKE_SOURCE_DIR points at the consumer's root, not this repo).

Change 2 (lib/CMakeLists.txt): extend the Android bundled-deps legacy path to
also cover iOS. A cross-compile cannot reliably find a system libsqlite3, and
the vendored zlib renames its exports to act_z_* (zlib/names.h) so a system
libz cannot satisfy those symbols. iOS now builds the vendored sqlite
amalgamation + bundled zlib, matching Android. Only affects legacy mode
(MATSDK_USE_VCPKG_DEPS=OFF); the vcpkg Apple path still links system libs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Explicitly disable warning-as-error for the vendored SQLite TU on MSVC

Address Copilot review comment on lib/CMakeLists.txt:470. The comment claimed
the build drops /WX for the vendored SQLite translation unit, but the code only
added /w. /w disables all warnings, but MSVC can still promote a non-suppressible
warning to an error under an inherited /WX. Add /WX- so the code literally
matches the comment's stated intent and cannot be broken by such a warning.
Verified cl.exe accepts /w /WX- together.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify MATSDK_MINIMAL_SQLITE-on-Apple comment for the iOS legacy path

Address Copilot review comment on lib/CMakeLists.txt:446. Adding iOS to the
legacy bundled-SQLite path means MATSDK_MINIMAL_SQLITE is no longer a strict
no-op on all Apple builds: iOS in legacy mode (MATSDK_USE_VCPKG_DEPS=OFF)
bundles the amalgamation and applies the strip definitions to it, matching
Android legacy. Clarify the comment so it no longer reads as a blanket
'no effect on Apple' statement. No behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Link system sqlite3 + zlib on Apple legacy builds (match repo convention)

Take further inspiration from the ONNX Runtime consumer patch, verified against
what the repo already does on Apple.

The SDK's own iOS Xcode projects link libsqlite3.tbd + libz.tbd from the SDKROOT,
Package.swift links .linkedLibrary("sqlite3")/("z"), and microsoft#1499 already links the
system libsqlite3/libz on the vcpkg Apple path. Bundling is an Android-only
convention (the NDK ships no system zlib). So the earlier change that made iOS
legacy bundle sqlite+zlib was the inconsistent one; this aligns iOS with the rest
of the repo.

- Apple legacy (macOS + iOS) now links system `sqlite3 z` by portable names in a
  single elseif(APPLE) branch. macOS moves off find_package(ZLIB) + hardcoded
  Homebrew .a paths (non-relocatable) onto the same portable link names, so
  exported static packages stay relocatable. iOS no longer bundles.
- iOS dropped from the MATSDK_BUNDLE_SQLITE gating and the bundled-zlib branch,
  which are now Android-only.
- Exclude iOS from include_directories(/usr/local/include): that host (macOS) path
  must not be injected into an iOS cross-compile's search path where it can shadow
  the iOS SDK's own headers.
- Linux legacy simplified to find_package(SQLite3) (the Homebrew .a fallbacks were
  macOS-only and are now handled by the Apple branch).

Verified: Linux top-level and add_subdirectory legacy builds both produce
libmat.so. The Apple legacy path is exercised by the macOS-latest CI leg
(build-posix-latest, legacy mode); iOS cannot be built on this host.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Drop redundant ZLIB include dir on the Linux legacy path

target_include_directories(mat PRIVATE ${ZLIB_INCLUDE_DIRS}) was redundant: mat
already links ZLIB::ZLIB (and SQLite::SQLite3), imported targets that propagate
their own include directories. Verified: Linux legacy mat build still resolves
<zlib.h> and produces libmat.so.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* vcpkg port: bump pinned REF to v3.10.173.1 to match the SDK version

The port pinned v3.10.161.1 while the SDK source on this branch is at
v3.10.173.1 (Version.hpp), leaving production installs two releases behind.
Bump the portfile REF + SHA512 and the vcpkg.json version to v3.10.173.1;
the SHA512 is computed from the release source tarball.

Note: the port's minimal-sqlite and Apple system-sqlite features depend on
CMake changes introduced by this PR that are not yet in any release tag. The
in-repo port tests exercise them against local source via
MATSDK_VCPKG_SOURCE_DIR, and the pinned REF must be advanced again to the
release that includes these changes once it is cut.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Validate vcpkg release bump production port path

After updating the vcpkg port REF/SHA512/version for a new SDK release,
exercise the real production port path with MATSDK_VCPKG_SOURCE_DIR unset.
This catches mismatches where the port manifest assumes source changes that
are not present in the release tag the port downloads.

When the new footprint features are present, validate the opt-in
minimal-sqlite + curl-openssl feature set so release automation covers both
release pinning and feature wiring before opening the vcpkg PR.

Validation:
- Parsed .github/workflows/vcpkg-release-bump.yml with PyYAML.
- Verified the feature-selection expression resolves to
  cpp-client-telemetry[core,minimal-sqlite,curl-openssl] for the current port.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove vcpkg release-bump production validation

Remove the release-bump production-port validation added in 93dd36e.
The vcpkg port update will instead rely on the explicit release sequencing:
merge the SDK source changes, cut a new SDK tag, then bump the vcpkg REF,
SHA512, and version to that tag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert "Remove vcpkg release-bump production validation"

This reverts commit dd6007a.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…der (microsoft#1512)

* Replace malformed UTF-8 instead of throwing in PayloadDecoder

* Address review: reword comment and add PayloadDecoder regression tests

- Reword the DecodeRequest comment to state the local rationale for
  error_handler_t::replace instead of referencing the HAVE_MAT_AI-gated
  AIJsonSerializer, which is not part of the default OSS build.
- Add PayloadDecoderTests covering the invalid-UTF-8 regression: a record
  whose string field carries a non-UTF-8 byte must be decoded without
  throwing type_error.316, and the bad byte must surface as U+FFFD. Also
  verifies valid UTF-8 is left untouched.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6ab3a16b-98f7-4fa7-a933-684b34196436

* Harden PayloadDecoder tests against -Werror char conversion

Address review feedback on the regression tests:

- Inject the invalid UTF-8 byte via push_back(static_cast<char>(0xFF))
  and search for it with static_cast<char>(0xFF) instead of a '\xFF'
  string/char literal, which relies on implementation-defined char
  conversion and can trip -Werror constant-conversion on some toolchains.
- Drop the product-specific phrasing from the test comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6ab3a16b-98f7-4fa7-a933-684b34196436

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6ab3a16b-98f7-4fa7-a933-684b34196436
…icrosoft#1509)

Broken normalization in the lib/modules subrepo is causing enlistment
issues in the Edge project; rolling forward the submodule to the commit
7bd8b516e2d93d1704834e0895733ae7bc2d1f43 picks up fixes made in that
repo that amend this.

Co-authored-by: bmehta001 <bmehta001@users.noreply.github.com>
* Add Android HTTP transport selection for vcpkg

Decouple Android HTTP transport choice from MATSDK_USE_VCPKG_DEPS so consumers can use vcpkg-provided native dependencies while still packaging the Java/JNI transport.

Files changed:

- CMakeLists.txt, lib/CMakeLists.txt, cmake/MSTelemetryConfig.cmake.in: add MATSDK_ANDROID_HTTP_CLIENT and export Android Java source metadata.

- tools/ports/cpp-client-telemetry/*, docs/building-with-vcpkg.md: add android-java-http feature, source guard, and consumer documentation.

- lib/pal/posix/*_Android.cpp: align initializer order for current NDK Clang builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b12c5862-01e3-45e4-bf91-6389c20cae41

* Default Android vcpkg builds to Java HTTP

Make Android AUTO select the Java/JNI HTTP transport regardless of dependency sourcing, and require explicit android-curl-* features for native curl.

Files changed:

- CMakeLists.txt: resolve Android AUTO to JAVA.

- tools/ports/cpp-client-telemetry/portfile.cmake: use the in-repo checkout for overlay builds and add explicit Android curl feature handling.

- tools/ports/cpp-client-telemetry/vcpkg.json: move Android curl to explicit android-curl-openssl/android-curl-mbedtls features.

- docs/building-with-vcpkg.md: document Java default and curl escape hatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b12c5862-01e3-45e4-bf91-6389c20cae41

* Address Copilot review for Android transport PR

NetworkInformationImpl_Android.cpp: initialize m_registeredCount so callback registration bookkeeping does not read an indeterminate value. Verified m_registeredCount is incremented/decremented in lib/pal/NetworkInformationImpl.hpp.

DeviceInformationImpl_Android.cpp: initialize m_registeredCount for the same callback bookkeeping path. Verified m_registeredCount is incremented/decremented in lib/pal/DeviceInformationImpl.hpp.

MSTelemetryConfig.cmake.in: update the curl dependency comment to reflect Linux, explicit Android curl builds, and macOS-without-Apple-HTTP only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b12c5862-01e3-45e4-bf91-6389c20cae41

* Address Copilot follow-up review for Android transport

portfile.cmake: make in-repo overlay source detection require SDK-specific paths so registry/vcpkg checkouts cannot be mistaken for cpp_client_telemetry.

MSTelemetryConfig.cmake.in: set MSTelemetry_ANDROID_JAVA_SOURCE_DIR only for Java-transport packages; set it empty otherwise.

Android PAL: initialize m_type and m_os_architecture alongside the callback counters so inline getters never read indeterminate values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b12c5862-01e3-45e4-bf91-6389c20cae41

* Address final Copilot comments on Android transport

MSTelemetryConfig.cmake.in: export NONE instead of an empty Android transport sentinel for non-Android packages, while keeping the Java source directory empty outside JAVA transport.

portfile.cmake: add the [core,...]/default-features=false remediation hint to the curl backend mutual-exclusivity error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b12c5862-01e3-45e4-bf91-6389c20cae41

---------

Copilot-Session: b12c5862-01e3-45e4-bf91-6389c20cae41
…crosoft#1506)

* Create the offline event cache with owner-only (0600) permissions

The SQLite offline cache buffers pending telemetry events (tenant ids, user
identifiers, serialized event payloads) but was created with SQLite's default
file permissions (0644 -- world-readable), letting any co-located user on a POSIX
system read the buffered event stream or tamper with pending events.

Restrict the database file to 0600 immediately after opening it in
SQLiteWrapper::open. This runs before WAL is enabled, so the -wal/-journal
companion files inherit 0600 from the main database file (SQLite's
findCreateFileMode derives their mode from the main db). The chmod is best-effort
(a failure, e.g. an in-memory ":memory:" database, does not fail the open) and
POSIX-only -- on Windows the Unix mode bits are meaningless (NTFS ACLs govern
access).

Adds a POSIX unit test asserting the cache and its companions are not group/world
accessible. Verified on Linux (umask 022): a bare SQLite db is created 0644; with
this change the cache and its -wal/-journal are 0600.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Also tighten pre-existing companion files on open (round 1)

Address Copilot review: only the main database was chmod'd, so a cache
created by an older SDK (or a companion left behind after a crash) could
retain the old world-readable 0644 mode on its -wal/-shm/-journal files.
Best-effort chmod any pre-existing companions to 0600 on open, ignoring
ENOENT. Add ExistingFilesAreTightenedOnOpen covering the migration path
(loosen db + plant a leftover -wal, then verify reopen re-tightens both).

Files:
- lib/offline/SQLiteWrapper.hpp
- tests/unittests/OfflineStorageTests_SQLite.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Silence in-memory chmod warning and clean up test companions (round 2)

Address Copilot review round 2:
- The main-database chmod now ignores ENOENT, so opening an in-memory
  (":memory:") database -- which LogManagerImpl uses when no tenant token is
  configured -- no longer logs a spurious permission-tightening warning on
  every open (there is no file to secure).
- ExistingFilesAreTightenedOnOpen now removes the database and its companion
  files (including the planted -wal) at the end, so they don't leak into other
  tests that reuse the same storage filename.

Files:
- lib/offline/SQLiteWrapper.hpp
- tests/unittests/OfflineStorageTests_SQLite.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove WAL companion files in shared test teardown (round 3)

Address Copilot review round 3: CacheFileCreatedOwnerReadWriteOnly (and any
other WAL-mode test) relied on shutdownAndRemoveFile(), which only removed the
main database and could leave -wal/-shm/-journal companions behind to pollute
the temp dir. Remove the companions in the shared teardown helper so every
test is covered, and drop the now-redundant explicit cleanup from
ExistingFilesAreTightenedOnOpen.

Files:
- tests/unittests/OfflineStorageTests_SQLite.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…icrosoft#1507)

* Cap curl HTTP response body size to prevent memory-amplification DoS

The libcurl transport buffered the entire collector response with no upper
bound: WriteMemoryCallback and WriteVectorCallback grew (realloc / vector
insert) until the whole body was received. A hostile or MITM'd collector
could return an arbitrarily large body and drive unbounded memory growth in
the embedding process. OneCollector responses are tiny (status, kill-switch
tokens, retry-after, small config), so a generous fixed cap never rejects a
legitimate response.

Add a 16 MB cap (kMaxResponseBytes) enforced overflow-safely in both curl
write callbacks; exceeding it returns a short count so curl aborts the
transfer with CURLE_WRITE_ERROR (the upload becomes a network failure and is
retried). Add HttpClientCurlResponseCapTests covering the oversized-abort and
large-under-cap paths.

Files:
- lib/http/HttpClient_Curl.hpp
- tests/unittests/HttpClientCurlTests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Guard response cap against overflow and fix test request lifetime (round 1)

Address Copilot review on the response-size cap:
- Guard the size * nmemb product in both curl write callbacks against size_t
  multiplication overflow before using it, so the "overflow-safe" cap check
  cannot operate on a wrapped length.
- The response-cap test fixture now owns the IHttpRequest (the client only
  stores a raw pointer and never frees it) and releases it in TearDown on the
  main thread, fixing the per-test leak. Freeing it in OnHttpResponse would
  destroy the CurlHttpOperation from within its own async task (whose
  destructor waits on that task -- a self-join deadlock), so teardown-time
  release is used instead. sendAndWait() also resets result state up front so
  the helper is safe to reuse.

Files:
- lib/http/HttpClient_Curl.hpp
- tests/unittests/HttpClientCurlTests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Synchronize test response-size field to avoid a data race (round 2)

Address Copilot review round 2: m_responseBodySize was written by the test
thread in sendAndWait() and read by the HttpServer reactor thread in
onHttpRequest() without synchronization -- a data race that would trip TSAN.
Write it under the existing mutex and read it under the same lock (into a
local) to establish a happens-before edge.

Files:
- tests/unittests/HttpClientCurlTests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rosoft#1505)

String.prototype.trim() (added in microsoft#1455 to complete the newline sanitization) is not
implemented by the Windows Script Host JScript engine that runs version.js via
cscript, so every Windows CI build logged 'version.js(48,3) Microsoft JScript
runtime error: Object doesn't support this property or method'. Replace trim() with
an ES3-compatible global-anchored regex that strips leading/trailing whitespace
(including all trailing newlines), so the script runs cleanly while keeping the
complete-sanitization behavior CodeQL asked for.

Verified: 'cscript //nologo version.js' now exits 0 and regenerates Version.hpp;
confirmed the JScript engine rejects .trim() with the same error seen in CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ts (microsoft#1508)

* Cap HTTP response body size across WinInet, WinRt, and Apple transports

Extends the memory-amplification DoS hardening (a hostile or MITM'd collector
returning an oversized body to exhaust process memory) beyond the libcurl
transport to the remaining platform transports. Introduces a single shared
constant MAX_HTTP_RESPONSE_SIZE (16 MB) in IHttpClient.hpp so every transport
uses the same generous ceiling, well above any legitimate OneCollector or
config response.

- WinInet: bound m_bodyBuffer in the InternetReadFile loop; over-cap aborts
  the read and the request is reported as a failure (retried).
- WinRt: reject a ReadAsBufferAsync buffer whose length exceeds the cap
  without copying it; report NetworkFailure.
- Apple (NSURLSession): reject a completion-handler NSData larger than the cap
  without copying it; report NetworkFailure.

The libcurl transport is capped separately in its own focused change; a later
cleanup can unify its constant onto MAX_HTTP_RESPONSE_SIZE.

Files:
- lib/include/public/IHttpClient.hpp
- lib/http/HttpClient_WinInet.cpp
- lib/http/HttpClient_WinRt.cpp
- lib/http/HttpClient_Apple.mm

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Map WinInet oversize to NetworkFailure and guard Apple nil data (round 1)

Address Copilot review:
- WinInet: the oversize-response abort set ERROR_NOT_ENOUGH_MEMORY, which fell
  through to the default case (LocalFailure). Use ERROR_HTTP_INVALID_SERVER_-
  RESPONSE so it maps to HttpResult_NetworkFailure, consistent with the WinRt
  and Apple transports (still retried, but correctly classified).
- Apple: guard the success-path copy on a non-zero length and cast data.length
  to size_t, so a nil NSData (bytes == nullptr) never performs pointer
  arithmetic on nullptr (undefined behavior).

Files:
- lib/http/HttpClient_WinInet.cpp
- lib/http/HttpClient_Apple.mm

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Stream response bodies to enforce the cap before full materialization (round 2)

Address Copilot review round 2: the previous checks ran after the framework had
already materialized the whole response body, so an oversized response could
still drive a large allocation. Rework each transport to bound memory to the cap:

- WinInet: check before every append (pre-loop and in-loop) so m_bodyBuffer
  never exceeds MAX_HTTP_RESPONSE_SIZE, not "cap + one chunk". (Validated: the
  Windows `mat` library compiles.)
- WinRt: request with HttpCompletionOption::ResponseHeadersRead (so the body is
  not pre-buffered) and stream it via ReadAsInputStreamAsync in 64 KB chunks,
  aborting the moment the cap would be exceeded.
- Apple: replace the completionHandler NSURLSession API (which materializes the
  full NSData) with a streaming NSURLSessionDataDelegate that accumulates in
  didReceiveData: and cancels the task once the cap would be exceeded; an
  over-cap transfer is surfaced as NetworkFailure (retried).

The WinRt and Apple rewrites target UWP/macOS toolchains that aren't available
locally, so they are review-verified and must be built/tested on-device before
merge.

Files:
- lib/http/HttpClient_WinInet.cpp
- lib/http/HttpClient_WinRt.cpp
- lib/http/HttpClient_Apple.mm

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden streaming transports: guard WinRt read faults, fix Apple block cast (round 3)

Address Copilot review round 3 on the streaming rework:
- WinRt: concurrency::task::wait()/get() rethrow if ReadAsInputStreamAsync or a
  chunk ReadAsync faults (e.g., connection reset) even when the status looks
  completed. Wrap the whole streamed-read in try/catch so a fault maps to
  HttpResult_NetworkFailure instead of escaping onRequestComplete and crashing.
- Apple: cast the dictionary value (stored as id) back to the concrete block
  type in didCompleteWithError: to avoid an incompatible-pointer-types warning
  (which fails builds under -Werror).

Files:
- lib/http/HttpClient_WinRt.cpp
- lib/http/HttpClient_Apple.mm

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* WinRt: map cancel to Aborted and clear partial body on rejection (round 4)

Address Copilot review round 4: HttpResponseDecoder processes any non-empty
response body regardless of HttpResult (processBody runs when GetBody() is
non-empty), so a partial body left on a rejected streamed response could be
parsed for kill-switch/stats. In the WinRt streaming reader:
- Map a caller-initiated cancellation (task_status::canceled, from cancel())
  to HttpResult_Aborted instead of NetworkFailure.
- Clear response->m_body on every non-success path (cancel, read failure,
  over-cap, and streaming exceptions) so no partial body is processed.

(WinInet and Apple never attach a partial body to the response on rejection,
so they need no change.)

Files:
- lib/http/HttpClient_WinRt.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Apple response-cap delegate cleanup: NSMutableSet uses removeObject:

The streaming session delegate's didCompleteWithError: cleanup called
[_overCap removeObjectForKey:key], but _overCap is an NSMutableSet, which
has no removeObjectForKey: selector (that belongs to NSMutableDictionary).
This was a copy-paste from the _handlers/_buffers dictionary cleanup two
lines above and fails to compile, breaking the entire Apple/macOS mat build.

Use the correct NSMutableSet selector, removeObject:.

Validation (macOS arm64, Apple HTTP transport):
- libmat builds clean; full host UnitTests 518/518 pass.
- End-to-end test against a local HttpServer through the real HttpClient_Apple:
  under-cap (64 KB) -> HttpResult_OK with full body; over-cap (16 MB + 1 MB)
  -> HttpResult_NetworkFailure with an empty body and no crash; exactly
  MAX_HTTP_RESPONSE_SIZE (16 MB) -> HttpResult_OK with full body. Stable over
  8 repeats (no delegate state races).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t#1503)

* Add a public-header gate CI job; make CompliantByDefaultFilterApi self-contained

Consumers that embed the SDK (e.g. ONNX Runtime / Foundry Local) compile their
own translation units -- which include our public headers -- under strict
warning flags: -Wall -Wextra -Werror on GCC/Clang (plus -Wshorten-64-to-32 on
Clang) and /W4 /WX on MSVC, suppressing third-party headers via -isystem /
/external:W0. When that suppression is defeated (include order, PCH, or
NO_SYSTEM_FROM_IMPORTED), any warning or missing include in our headers breaks
the consumer build.

This adds a CI gate that compiles every public header on its own, with no
-isystem suppression, under those flags on GCC, Clang, and MSVC, so header
issues surface here instead of at integration time:
- tests/headers/check_public_headers.sh  (GCC + Clang)
- tests/headers/check_public_headers.cmd (MSVC)
- .github/workflows/public-header-gate.yml

The gate caught one real self-containment bug: CompliantByDefaultFilterApi.hpp
uses std::vector<uint8_t> but did not include <cstdint>, so it only compiled when
something else pulled <cstdint> in first. Added the include. VariantType.hpp is
excluded: it is an implementation fragment included by Variant.hpp (which defines
VariantMap/VariantArray and the needed std headers first), not a standalone header.

Validated locally: all 40 public headers pass (39 compiled + VariantType
excluded) on g++, clang++, and MSVC cl /W4 /WX.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Copilot round 1 on microsoft#1503: gate .h headers, tighten flags, fix MSVC includes

- Gate both *.hpp and *.h so the flat C API headers (mat.h, CommonFields.h) are
  covered, not just *.hpp. Both compile clean standalone under the gate flags.
- Drop -Wno-unused-but-set-variable from the GCC/Clang flags: no public header
  relies on it, so removing it makes the gate stricter. Kept -Wno-unused-parameter
  (mirrors this repo's WARN_FLAGS; intentional unused params use UNREFERENCED_PARAMETER).
- MSVC gate: add /I lib\include so headers that conditionally pull mat/config.h
  (e.g. CsProtocol_types.hpp) exercise the same include graph as the GCC/Clang gate.

Validated locally: 41/41 public headers pass on g++, clang++, and MSVC cl /W4 /WX.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fold the public-header gate into existing CI workflows

Instead of a standalone workflow, add the header gate as an isolated job in the
workflows that already run on the same triggers:
- build-posix-latest.yml gains a 'public-headers' job (GCC/Clang on ubuntu).
- test-win-latest.yml gains a 'public-headers' job (MSVC on windows).

Removes .github/workflows/public-header-gate.yml. The gate scripts under
tests/headers/ are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Header gate: drop -Wno-unused-parameter so it mirrors real consumer flags

The gate suppressed -Wunused-parameter, which is not part of the strict
consumer flag set it claims to mirror (-Wall -Wextra -Werror). That hid a real
break: NullObjects.hpp overrides left parameters unused because the old
UNREFERENCED_PARAMETER macro expanded to nothing on GCC/Clang, so a consumer
including LogManager.hpp with plain -Wall -Wextra -Werror failed to compile
while the gate passed. With the macro now expanding to (void)(...) (merged from
main), the headers are clean without the suppression; drop it so the gate
actually catches this class of consumer break.

Verified: 41/41 public headers pass standalone on g++ and clang++ without
-Wno-unused-parameter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fail the public-header gate when no headers are found or PUB is missing

The gate could silently pass without compiling anything: with nullglob the shell
header globs expand to nothing (and the batch FOR loop runs zero times) when the
computed public-header directory is wrong, so a miscomputed path reported success
while testing nothing. Both scripts now validate the public-header directory
exists and fail if zero headers were compiled.

Also give the MSVC script a unique per-invocation work directory
(%TEMP%\pubhdrgate_<rand>) so concurrent runs on the same machine cannot clobber
each other's temporary translation unit, and clean it up on exit.

Files: tests/headers/check_public_headers.sh, tests/headers/check_public_headers.cmd

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Strengthen public header gate coverage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use precise wall-clock time where available and retain nanosecond-derived 100 ns ticks on POSIX so record.time no longer truncates every event to milliseconds. Add regression coverage for POSIX timestamp precision.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 05d1030e-75b0-447f-9856-65091d59a97f
Resolve GetSystemTimePreciseAsFileTime once instead of repeating module and symbol lookups for every event timestamp.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 05d1030e-75b0-447f-9856-65091d59a97f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Event timestamps (record.time) only have millisecond resolution despite 100ns tick field

6 participants