@@ -107,16 +107,26 @@ fn should_skip_placeholder(file_attributes: u32, policy: PlaceholderPolicy) -> b
107107 && file_attributes & FILE_ATTRIBUTE_RECALL_ON_OPEN != 0
108108}
109109
110- /// Whether `path` carries one or more NTFS Alternate Data Streams beyond its
111- /// main unnamed `::$DATA` stream (DESIGN s5.2.1, STRESS_HARNESS s3.5
112- /// `ads-alternate-data-stream`).
110+ /// Whether `path` carries one or more WARN-WORTHY NTFS Alternate Data Streams
111+ /// beyond its main unnamed `::$DATA` stream (DESIGN s5.2.1, STRESS_HARNESS
112+ /// s3.5 `ads-alternate-data-stream`).
113113///
114114/// Driven backs up the main stream only; a file with named streams (e.g.
115115/// `foo.txt:secret`) silently loses those streams. The scanner detects them
116116/// so the orchestrator can surface a one-per-file `local.ads_skipped` warning
117117/// (SPEC s24) rather than dropping them silently - silent data loss in a
118118/// backup tool.
119119///
120+ /// EXEMPTION: `Zone.Identifier` - the mark-of-the-web tag Windows attaches to
121+ /// every downloaded file - is deliberately NOT warn-worthy. It is transient
122+ /// browser provenance metadata, not user data; a Documents tree full of
123+ /// downloads carries it on thousands of files, and warning on each buried the
124+ /// real signal in the 2026-08-14 incident's logs (14,098 of that day's 14,174
125+ /// lines were `ads_skipped`). A file whose ONLY named stream is
126+ /// `Zone.Identifier` returns `false`; any other named stream still warns,
127+ /// Zone.Identifier alongside it or not. Zero extra I/O either way: the stream
128+ /// NAMES arrive in the same enumeration this probe already runs.
129+ ///
120130/// Windows enumerates streams via `FindFirstStreamW` / `FindNextStreamW`
121131/// (`STREAM_INFO_LEVELS::FindStreamInfoStandard`). The main stream reports as
122132/// `::$DATA`; any other `:<name>:$DATA` entry is an ADS. Non-Windows targets
@@ -149,12 +159,29 @@ fn has_alternate_data_streams(path: &Path) -> bool {
149159 fn FindClose ( handle : isize ) -> i32 ;
150160 }
151161
162+ // The stream name up to its NUL terminator.
163+ fn stream_name ( name : & [ u16 ] ) -> & [ u16 ] {
164+ let len = name. iter ( ) . position ( |& c| c == 0 ) . unwrap_or ( name. len ( ) ) ;
165+ & name[ ..len]
166+ }
167+
152168 // The unnamed main stream's name, as FindFirstStreamW reports it.
153169 fn is_main_stream ( name : & [ u16 ] ) -> bool {
154- // Compare against the literal "::$DATA" up to the first NUL.
155170 let main: Vec < u16 > = "::$DATA" . encode_utf16 ( ) . collect ( ) ;
156- let len = name. iter ( ) . position ( |& c| c == 0 ) . unwrap_or ( name. len ( ) ) ;
157- name[ ..len] == main[ ..]
171+ stream_name ( name) == main
172+ }
173+
174+ // The mark-of-the-web tag (see the fn doc's EXEMPTION). NTFS stream names
175+ // are case-insensitive and this one is pure ASCII, so an ASCII
176+ // case-insensitive compare is exact.
177+ fn is_zone_identifier_stream ( name : & [ u16 ] ) -> bool {
178+ const ZONE : & str = ":Zone.Identifier:$DATA" ;
179+ let name = stream_name ( name) ;
180+ name. len ( ) == ZONE . len ( )
181+ && name
182+ . iter ( )
183+ . zip ( ZONE . bytes ( ) )
184+ . all ( |( & c, e) | c < 128 && ( c as u8 ) . eq_ignore_ascii_case ( & e) )
158185 }
159186
160187 let wide: Vec < u16 > = path
@@ -180,7 +207,7 @@ fn has_alternate_data_streams(path: &Path) -> bool {
180207 }
181208 let mut found_ads = false ;
182209 loop {
183- if !is_main_stream ( & data. stream_name ) {
210+ if !is_main_stream ( & data. stream_name ) && ! is_zone_identifier_stream ( & data . stream_name ) {
184211 found_ads = true ;
185212 break ;
186213 }
@@ -1354,6 +1381,63 @@ mod tests {
13541381 use async_trait:: async_trait;
13551382
13561383 use super :: * ;
1384+
1385+ /// `Zone.Identifier` alone must NOT flag a file (log-noise follow-up to
1386+ /// the 2026-08-14 incident, where it accounted for ~all `ads_skipped`
1387+ /// warnings); any OTHER named stream still must, with or without a
1388+ /// Zone.Identifier alongside. Skips gracefully when the temp volume
1389+ /// cannot hold ADS (non-NTFS).
1390+ #[ cfg( windows) ]
1391+ #[ test]
1392+ fn ads_probe_exempts_zone_identifier_only_files ( ) {
1393+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1394+
1395+ let plain = dir. path ( ) . join ( "plain.txt" ) ;
1396+ fs:: write ( & plain, b"body" ) . unwrap ( ) ;
1397+ assert ! ( !has_alternate_data_streams( & plain) , "no streams, no flag" ) ;
1398+
1399+ let zoned = dir. path ( ) . join ( "zoned.txt" ) ;
1400+ fs:: write ( & zoned, b"body" ) . unwrap ( ) ;
1401+ if fs:: write (
1402+ format ! ( "{}:Zone.Identifier" , zoned. display( ) ) ,
1403+ b"[ZoneTransfer]\r \n ZoneId=3\r \n " ,
1404+ )
1405+ . is_err ( )
1406+ {
1407+ eprintln ! ( "temp volume does not support ADS; skipping" ) ;
1408+ return ;
1409+ }
1410+ assert ! (
1411+ !has_alternate_data_streams( & zoned) ,
1412+ "Zone.Identifier alone is exempt (mark-of-the-web metadata)"
1413+ ) ;
1414+
1415+ // NTFS stream names are case-insensitive; the exemption must be too.
1416+ let cased = dir. path ( ) . join ( "cased.txt" ) ;
1417+ fs:: write ( & cased, b"body" ) . unwrap ( ) ;
1418+ fs:: write ( format ! ( "{}:zone.identifier" , cased. display( ) ) , b"x" ) . unwrap ( ) ;
1419+ assert ! (
1420+ !has_alternate_data_streams( & cased) ,
1421+ "the exemption is case-insensitive"
1422+ ) ;
1423+
1424+ let secret = dir. path ( ) . join ( "secret.txt" ) ;
1425+ fs:: write ( & secret, b"body" ) . unwrap ( ) ;
1426+ fs:: write ( format ! ( "{}:secret" , secret. display( ) ) , b"hidden" ) . unwrap ( ) ;
1427+ assert ! (
1428+ has_alternate_data_streams( & secret) ,
1429+ "a real named stream still flags"
1430+ ) ;
1431+
1432+ let both = dir. path ( ) . join ( "both.txt" ) ;
1433+ fs:: write ( & both, b"body" ) . unwrap ( ) ;
1434+ fs:: write ( format ! ( "{}:Zone.Identifier" , both. display( ) ) , b"x" ) . unwrap ( ) ;
1435+ fs:: write ( format ! ( "{}:extra" , both. display( ) ) , b"y" ) . unwrap ( ) ;
1436+ assert ! (
1437+ has_alternate_data_streams( & both) ,
1438+ "Zone.Identifier must not mask another named stream"
1439+ ) ;
1440+ }
13571441 use crate :: state:: {
13581442 AccountRow , ActivityFilter , ActivityPage , FileSearchHit , FileStateRow , NewActivity ,
13591443 NewPendingOp , PageRequest , PendingOpRow ,
0 commit comments