Skip to main content

atman_runtime/tools/
anchor_fs.rs

1//! Hashline anchors and durable snapshot/change state.
2use std::collections::HashSet;
3use std::fs::{self, OpenOptions};
4use std::io::{self, Write};
5use std::path::{Path, PathBuf};
6use std::thread;
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
9use rusqlite::{Connection, Error as SqlError, ErrorCode, OptionalExtension, params};
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use thiserror::Error;
13
14pub const HASH_ALGORITHM: &str = "anchor-v1";
15const BASE: u32 = 62;
16const ANCHOR_SPACE: u32 = BASE * BASE * BASE;
17const PROBE_STRIDE: u32 = BASE * BASE + BASE + 1;
18const ALPHABET: &[u8; 62] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
19
20#[derive(Debug, Error)]
21pub enum AnchorError {
22    #[error("I/O error for {path}: {source}")]
23    Io {
24        path: PathBuf,
25        #[source]
26        source: io::Error,
27    },
28    #[error("SQLite error for {path}: {source}")]
29    Sql {
30        path: PathBuf,
31        #[source]
32        source: SqlError,
33    },
34    #[error("invalid UTF-8 in {path}")]
35    InvalidUtf8 { path: PathBuf },
36    #[error("missing file: {0}")]
37    MissingFile(PathBuf),
38    #[error("corrupt state at {path}: {reason}")]
39    CorruptState { path: PathBuf, reason: String },
40    #[error("strict undo refused for {path}: current hash is {actual}, expected {expected}")]
41    UndoConflict {
42        path: PathBuf,
43        expected: String,
44        actual: String,
45    },
46    #[error("strict anchor resolution failed for {path}: {reason}")]
47    Resolve { path: PathBuf, reason: String },
48    #[error("change not found: {0}")]
49    ChangeNotFound(String),
50    #[error("file hash conflict for {path}: current hash is {actual}, expected {expected}")]
51    HashConflict {
52        path: PathBuf,
53        expected: String,
54        actual: String,
55    },
56    #[error("atomic replace is unsupported on Windows without ReplaceFileW: {0}")]
57    UnsupportedAtomicReplace(PathBuf),
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61pub struct AnchorLine {
62    pub anchor: String,
63    pub hash: String,
64    pub line: String,
65}
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct Snapshot {
68    pub snapshot_id: String,
69    pub path: String,
70    pub checksum: String,
71    pub line_count: usize,
72    pub hashes: Vec<String>,
73    pub hash_algorithm: String,
74    pub updated_at: u64,
75    pub anchors: Vec<String>,
76}
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78pub struct ChangeRecord {
79    pub change_id: String,
80    pub path: String,
81    pub before_hash: String,
82    pub after_hash: String,
83    pub before_content: String,
84    pub after_content: String,
85    pub parent_change_id: Option<String>,
86    pub created_at: u64,
87}
88
89pub struct StateStore {
90    root: PathBuf,
91}
92impl StateStore {
93    pub fn new(root: impl Into<PathBuf>) -> Self {
94        Self { root: root.into() }
95    }
96    fn db_path(&self) -> PathBuf {
97        self.root.join("anchor-state.sqlite3")
98    }
99    fn connect(&self) -> Result<Connection, AnchorError> {
100        fs::create_dir_all(&self.root).map_err(|e| io_error(&self.root, e))?;
101        let path = self.db_path();
102        match self.open_connection(&path).and_then(|c| {
103            init_schema(&c, &path)?;
104            Ok(c)
105        }) {
106            Ok(c) => Ok(c),
107            Err(first) if path.exists() => {
108                let corrupt = self
109                    .root
110                    .join(format!("anchor-state.sqlite3.corrupt-{}", now()));
111                let _ = fs::rename(&path, &corrupt);
112                let _ = fs::remove_file(format!("{}-wal", path.display()));
113                let _ = fs::remove_file(format!("{}-shm", path.display()));
114                self.open_connection(&path)
115                    .and_then(|c| {
116                        init_schema(&c, &path)?;
117                        Ok(c)
118                    })
119                    .map_err(|_| first)
120            }
121            Err(e) => Err(e),
122        }
123    }
124    fn open_connection(&self, path: &Path) -> Result<Connection, AnchorError> {
125        let c = retry_busy(|| Connection::open(path), path)?;
126        retry_busy(|| c.busy_timeout(Duration::from_millis(2500)), path)?;
127        retry_busy(|| c.pragma_update(None, "journal_mode", "WAL"), path)?;
128        retry_busy(|| c.pragma_update(None, "synchronous", "NORMAL"), path)?;
129        Ok(c)
130    }
131    pub fn snapshot(&self, path: &Path) -> Result<Option<Snapshot>, AnchorError> {
132        let c = self.connect()?;
133        let mut q = c
134            .prepare("SELECT data FROM snapshots WHERE path=?1")
135            .map_err(|e| sql_error(&self.db_path(), e))?;
136        let value = q
137            .query_row(params![path.to_string_lossy()], |r| r.get::<_, Vec<u8>>(0))
138            .optional()
139            .map_err(|e| sql_error(&self.db_path(), e))?;
140        value
141            .map(|b| serde_json::from_slice(&b).map_err(|e| corrupt(&self.db_path(), e)))
142            .transpose()
143    }
144    pub fn put_snapshot(&self, snapshot: Snapshot) -> Result<(), AnchorError> {
145        validate_snapshot(&snapshot)?;
146        let data = serde_json::to_vec(&snapshot).map_err(|e| corrupt(&self.db_path(), e))?;
147        let c = self.connect()?;
148        retry_busy(|| c.execute("INSERT INTO snapshots(path,data) VALUES(?1,?2) ON CONFLICT(path) DO UPDATE SET data=excluded.data", params![snapshot.path, data]), &self.db_path()).map(|_| ())
149    }
150    pub fn record_change(&self, change: ChangeRecord) -> Result<(), AnchorError> {
151        let data = serde_json::to_vec(&change).map_err(|e| corrupt(&self.db_path(), e))?;
152        let c = self.connect()?;
153        retry_busy(|| c.execute("INSERT OR REPLACE INTO changes(change_id,path,created_at,data) VALUES(?1,?2,?3,?4)", params![change.change_id, change.path, change.created_at, data]), &self.db_path()).map(|_| ())
154    }
155    pub fn latest_change(&self, path: &Path) -> Result<Option<ChangeRecord>, AnchorError> {
156        let c = self.connect()?;
157        let data = c.query_row("SELECT data FROM changes WHERE path=?1 ORDER BY created_at DESC, change_id DESC LIMIT 1", params![path.to_string_lossy()], |r| r.get::<_, Vec<u8>>(0)).optional().map_err(|e| sql_error(&self.db_path(), e))?;
158        data.map(|b| serde_json::from_slice(&b).map_err(|e| corrupt(&self.db_path(), e)))
159            .transpose()
160    }
161    pub fn change(&self, change_id: &str) -> Result<Option<ChangeRecord>, AnchorError> {
162        let c = self.connect()?;
163        let data = c
164            .query_row(
165                "SELECT data FROM changes WHERE change_id=?1",
166                params![change_id],
167                |r| r.get::<_, Vec<u8>>(0),
168            )
169            .optional()
170            .map_err(|e| sql_error(&self.db_path(), e))?;
171        data.map(|b| serde_json::from_slice(&b).map_err(|e| corrupt(&self.db_path(), e)))
172            .transpose()
173    }
174    pub fn undo_strict(
175        &self,
176        path: &Path,
177        change: &ChangeRecord,
178        current: &[u8],
179    ) -> Result<(), AnchorError> {
180        let actual = file_hash(current);
181        if actual != change.after_hash {
182            return Err(AnchorError::UndoConflict {
183                path: path.to_path_buf(),
184                expected: change.after_hash.clone(),
185                actual,
186            });
187        }
188        atomic_replace(path, change.before_content.as_bytes())
189    }
190}
191
192fn init_schema(c: &Connection, path: &Path) -> Result<(), AnchorError> {
193    c.execute_batch("CREATE TABLE IF NOT EXISTS snapshots(path TEXT PRIMARY KEY, data BLOB NOT NULL); CREATE TABLE IF NOT EXISTS changes(change_id TEXT PRIMARY KEY, path TEXT NOT NULL, created_at INTEGER NOT NULL, data BLOB NOT NULL); CREATE INDEX IF NOT EXISTS changes_path_created ON changes(path, created_at);").map_err(|e| sql_error(path, e))
194}
195fn retry_busy<T, F: FnMut() -> Result<T, SqlError>>(
196    mut f: F,
197    path: &Path,
198) -> Result<T, AnchorError> {
199    for attempt in 0..6 {
200        match f() {
201            Ok(v) => return Ok(v),
202            Err(e) if is_busy(&e) && attempt < 5 => {
203                thread::sleep(Duration::from_millis(10 * (attempt + 1)))
204            }
205            Err(e) => return Err(sql_error(path, e)),
206        }
207    }
208    unreachable!()
209}
210fn is_busy(e: &SqlError) -> bool {
211    matches!(e, SqlError::SqliteFailure(x, _) if matches!(x.code, ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked))
212}
213fn corrupt(path: &Path, e: impl std::fmt::Display) -> AnchorError {
214    AnchorError::CorruptState {
215        path: path.to_path_buf(),
216        reason: e.to_string(),
217    }
218}
219fn sql_error(path: &Path, e: SqlError) -> AnchorError {
220    AnchorError::Sql {
221        path: path.to_path_buf(),
222        source: e,
223    }
224}
225fn io_error(path: &Path, source: io::Error) -> AnchorError {
226    AnchorError::Io {
227        path: path.to_path_buf(),
228        source,
229    }
230}
231fn validate_snapshot(s: &Snapshot) -> Result<(), AnchorError> {
232    if s.hash_algorithm != HASH_ALGORITHM
233        || s.hashes.len() != s.line_count
234        || s.anchors.len() != s.line_count
235        || s.anchors
236            .iter()
237            .any(|a| a.len() != 3 || !a.bytes().all(|b| ALPHABET.contains(&b)))
238    {
239        return Err(corrupt(Path::new(&s.path), "invalid snapshot"));
240    }
241    Ok(())
242}
243
244pub fn file_hash(content: &[u8]) -> String {
245    let mut h = Sha256::new();
246    h.update(content);
247    format!("sha256:{:x}", h.finalize())
248}
249pub fn canonical_line(line: &str) -> &str {
250    line.strip_suffix('\r').unwrap_or(line).trim_end()
251}
252pub fn anchor_for_hash(hash: u32) -> String {
253    let mut v = hash % ANCHOR_SPACE;
254    let mut out = [b'A'; 3];
255    for i in (0..3).rev() {
256        out[i] = ALPHABET[(v % BASE) as usize];
257        v /= BASE;
258    }
259    String::from_utf8(out.to_vec()).unwrap()
260}
261pub fn line_hash(line: &str) -> u32 {
262    xxh32(canonical_line(line).as_bytes(), 0) >> 14
263}
264pub fn anchors_for_lines(
265    lines: &[String],
266    previous: Option<&Snapshot>,
267) -> Result<Vec<String>, AnchorError> {
268    let mut used = HashSet::new();
269    let mut result = Vec::with_capacity(lines.len());
270    for (i, line) in lines.iter().enumerate() {
271        let hash = format!("{:08x}", line_hash(line));
272        let mut value = previous
273            .and_then(|s| (s.hashes.get(i) == Some(&hash)).then(|| s.anchors[i].clone()))
274            .filter(|a| used.insert(a.clone()));
275        if value.is_none() {
276            let mut slot = line_hash(line) % ANCHOR_SPACE;
277            for _ in 0..ANCHOR_SPACE {
278                let a = anchor_for_hash(slot);
279                if used.insert(a.clone()) {
280                    value = Some(a);
281                    break;
282                }
283                slot = (slot + PROBE_STRIDE) % ANCHOR_SPACE;
284            }
285        }
286        result.push(value.ok_or_else(|| corrupt(Path::new("anchors"), "anchor space exhausted"))?);
287    }
288    Ok(result)
289}
290pub fn make_snapshot(
291    path: &Path,
292    content: &str,
293    previous: Option<&Snapshot>,
294) -> Result<Snapshot, AnchorError> {
295    let lines = split_lines(content);
296    let hashes = lines
297        .iter()
298        .map(|l| format!("{:08x}", line_hash(l)))
299        .collect();
300    let anchors = anchors_for_lines(&lines, previous)?;
301    let checksum = file_hash(content.as_bytes());
302    Ok(Snapshot {
303        snapshot_id: format!("snap_{}", &checksum[7..23]),
304        path: path.to_string_lossy().into(),
305        checksum,
306        line_count: lines.len(),
307        hashes,
308        hash_algorithm: HASH_ALGORITHM.into(),
309        updated_at: now(),
310        anchors,
311    })
312}
313pub fn split_lines(content: &str) -> Vec<String> {
314    if content.is_empty() {
315        Vec::new()
316    } else {
317        content.split_inclusive('\n').map(str::to_owned).collect()
318    }
319}
320
321/// Render the current file as stable hashline records.
322pub fn read_anchor_text(path: &Path, store: &StateStore) -> Result<String, AnchorError> {
323    let content = read_text(path)?;
324    let previous = store.snapshot(path)?;
325    let snapshot = make_snapshot(path, &content, previous.as_ref())?;
326    let lines = split_lines(&content);
327    let output = lines
328        .iter()
329        .enumerate()
330        .map(|(i, line)| {
331            format!(
332                "{}:{}│{}",
333                i + 1,
334                snapshot.anchors[i],
335                line.trim_end_matches(['\r', '\n'])
336            )
337        })
338        .collect::<Vec<_>>()
339        .join("\n");
340    store.put_snapshot(snapshot)?;
341    Ok(output)
342}
343
344/// The supported line-oriented mutation operation.
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub enum MutationOperation {
347    Replace,
348    Insert,
349    Remove,
350}
351
352impl MutationOperation {
353    fn parse(value: &str) -> Result<Self, AnchorError> {
354        match value {
355            "replace" => Ok(Self::Replace),
356            "insert" => Ok(Self::Insert),
357            "remove" => Ok(Self::Remove),
358            _ => Err(AnchorError::Resolve {
359                path: PathBuf::new(),
360                reason: format!("unknown operation {value}"),
361            }),
362        }
363    }
364}
365
366fn read_text(path: &Path) -> Result<String, AnchorError> {
367    let bytes = fs::read(path).map_err(|e| {
368        if e.kind() == io::ErrorKind::NotFound {
369            AnchorError::MissingFile(path.to_path_buf())
370        } else {
371            io_error(path, e)
372        }
373    })?;
374    String::from_utf8(bytes).map_err(|_| AnchorError::InvalidUtf8 {
375        path: path.to_path_buf(),
376    })
377}
378
379fn resolve_endpoint(
380    path: &Path,
381    snapshot: &Snapshot,
382    lines: &[String],
383    endpoint: &str,
384) -> Result<usize, AnchorError> {
385    if endpoint.len() != 3 || !endpoint.bytes().all(|byte| ALPHABET.contains(&byte)) {
386        return Err(AnchorError::Resolve {
387            path: path.to_path_buf(),
388            reason: format!("invalid endpoint {endpoint}; expected a 3-character anchor"),
389        });
390    }
391    let anchor = endpoint;
392    let matches: Vec<_> = snapshot
393        .anchors
394        .iter()
395        .enumerate()
396        .filter(|(_, candidate)| candidate.as_str() == anchor)
397        .map(|(index, _)| index)
398        .collect();
399    let raw_count = lines
400        .iter()
401        .filter(|line| anchor_for_hash(line_hash(line)) == anchor)
402        .count();
403    if raw_count > 1 {
404        return Err(AnchorError::Resolve {
405            path: path.to_path_buf(),
406            reason: format!("ambiguous endpoint {endpoint}"),
407        });
408    }
409    if matches.len() != 1 {
410        return Err(AnchorError::Resolve {
411            path: path.to_path_buf(),
412            reason: format!("stale or missing endpoint {endpoint}"),
413        });
414    }
415    Ok(matches[0])
416}
417
418/// Apply a strict anchor mutation and persist its inverse in the WAL.
419#[allow(clippy::too_many_arguments)]
420pub fn edit_by_anchor(
421    path: &Path,
422    operation: &str,
423    target: Option<&str>,
424    from: Option<&str>,
425    to: Option<&str>,
426    at: Option<&str>,
427    position: Option<&str>,
428    content: Option<&str>,
429    store: &StateStore,
430) -> Result<ChangeRecord, AnchorError> {
431    let op = MutationOperation::parse(operation).map_err(|e| AnchorError::Resolve {
432        path: path.to_path_buf(),
433        reason: e.to_string(),
434    })?;
435    let before = read_text(path)?;
436    let old_hash = file_hash(before.as_bytes());
437    let old_snapshot = store.snapshot(path)?;
438    let current_snapshot = make_snapshot(path, &before, old_snapshot.as_ref())?;
439    let old_lines = split_lines(&before);
440    let (start, end) = match op {
441        MutationOperation::Replace | MutationOperation::Remove => {
442            if let Some(t) = target {
443                let i = resolve_endpoint(path, &current_snapshot, &old_lines, t)?;
444                (i, i)
445            } else {
446                (
447                    resolve_endpoint(
448                        path,
449                        &current_snapshot,
450                        &old_lines,
451                        from.ok_or_else(|| AnchorError::Resolve {
452                            path: path.to_path_buf(),
453                            reason: "from is required".into(),
454                        })?,
455                    )?,
456                    resolve_endpoint(
457                        path,
458                        &current_snapshot,
459                        &old_lines,
460                        to.ok_or_else(|| AnchorError::Resolve {
461                            path: path.to_path_buf(),
462                            reason: "to is required".into(),
463                        })?,
464                    )?,
465                )
466            }
467        }
468        MutationOperation::Insert => {
469            let e = at.or(target).or(from).ok_or_else(|| AnchorError::Resolve {
470                path: path.to_path_buf(),
471                reason: "insert endpoint is required".into(),
472            })?;
473            let i = resolve_endpoint(path, &current_snapshot, &old_lines, e)?;
474            if position == Some("after") {
475                (i + 1, i + 1)
476            } else {
477                (i, i)
478            }
479        }
480    };
481    if start > end || end > old_lines.len() {
482        return Err(AnchorError::Resolve {
483            path: path.to_path_buf(),
484            reason: "range endpoints are reversed".into(),
485        });
486    }
487    let inserted = content.unwrap_or("");
488    let mut new_lines = old_lines[..start].to_vec();
489    if op != MutationOperation::Remove {
490        new_lines.extend(split_lines(inserted));
491    }
492    if op == MutationOperation::Insert {
493        new_lines.extend(old_lines[start..].iter().cloned());
494    } else {
495        new_lines.extend(old_lines[end + 1..].iter().cloned());
496    }
497    let after = new_lines.concat();
498    let new_hash = file_hash(after.as_bytes());
499    atomic_replace(path, after.as_bytes())?;
500    let snapshot = make_snapshot(path, &after, old_snapshot.as_ref())?;
501    store.put_snapshot(snapshot)?;
502    let change = ChangeRecord {
503        change_id: format!("change_{}_{}", now(), &new_hash[7..19]),
504        path: path.to_string_lossy().into(),
505        before_hash: old_hash,
506        after_hash: new_hash,
507        before_content: before,
508        after_content: after,
509        parent_change_id: store.latest_change(path)?.map(|c| c.change_id),
510        created_at: now(),
511    };
512    store.record_change(change.clone())?;
513    Ok(change)
514}
515
516pub fn overwrite_with_hash(
517    path: &Path,
518    expected_file_hash: &str,
519    content: &str,
520    store: &StateStore,
521) -> Result<ChangeRecord, AnchorError> {
522    let before = read_text(path)?;
523    let actual = file_hash(before.as_bytes());
524    if actual != expected_file_hash {
525        return Err(AnchorError::HashConflict {
526            path: path.to_path_buf(),
527            expected: expected_file_hash.into(),
528            actual,
529        });
530    }
531    let previous = store.snapshot(path)?;
532    atomic_replace(path, content.as_bytes())?;
533    let snapshot = make_snapshot(path, content, previous.as_ref())?;
534    store.put_snapshot(snapshot)?;
535    let after_hash = file_hash(content.as_bytes());
536    let change = ChangeRecord {
537        change_id: format!("change_{}_{}", now(), &after_hash[7..19]),
538        path: path.to_string_lossy().into(),
539        before_hash: actual,
540        after_hash,
541        before_content: before,
542        after_content: content.into(),
543        parent_change_id: store.latest_change(path)?.map(|c| c.change_id),
544        created_at: now(),
545    };
546    store.record_change(change.clone())?;
547    Ok(change)
548}
549
550pub fn undo_change(change_id: &str, store: &StateStore) -> Result<(), AnchorError> {
551    let change = store
552        .change(change_id)?
553        .ok_or_else(|| AnchorError::ChangeNotFound(change_id.into()))?;
554    let path = Path::new(&change.path);
555    let current = fs::read(path).map_err(|e| io_error(path, e))?;
556    store.undo_strict(path, &change, &current)?;
557    let previous = store.snapshot(path)?;
558    let restored = make_snapshot(path, &change.before_content, previous.as_ref())?;
559    store.put_snapshot(restored)
560}
561
562pub fn atomic_replace(path: &Path, bytes: &[u8]) -> Result<(), AnchorError> {
563    #[cfg(windows)]
564    {
565        let _ = bytes;
566        return Err(AnchorError::UnsupportedAtomicReplace(path.to_path_buf()));
567    }
568    #[cfg(unix)]
569    {
570        atomic_replace_unix(path, bytes)
571    }
572}
573#[cfg(unix)]
574fn atomic_replace_unix(path: &Path, bytes: &[u8]) -> Result<(), AnchorError> {
575    let parent = path.parent().unwrap_or(Path::new("."));
576    fs::create_dir_all(parent).map_err(|e| io_error(parent, e))?;
577    let tmp = parent.join(format!(
578        ".{}.anchor-tmp-{}",
579        path.file_name().and_then(|n| n.to_str()).unwrap_or("file"),
580        now()
581    ));
582    let result = (|| {
583        let mut f = OpenOptions::new()
584            .write(true)
585            .create_new(true)
586            .open(&tmp)
587            .map_err(|e| io_error(&tmp, e))?;
588        f.write_all(bytes).map_err(|e| io_error(&tmp, e))?;
589        f.sync_all().map_err(|e| io_error(&tmp, e))?;
590        fs::rename(&tmp, path).map_err(|e| io_error(path, e))
591    })();
592    if result.is_err() {
593        let _ = fs::remove_file(&tmp);
594    }
595    result
596}
597pub fn read_utf8(path: &Path) -> Result<String, AnchorError> {
598    let bytes = fs::read(path).map_err(|e| {
599        if e.kind() == io::ErrorKind::NotFound {
600            AnchorError::MissingFile(path.to_path_buf())
601        } else {
602            io_error(path, e)
603        }
604    })?;
605    String::from_utf8(bytes).map_err(|_| AnchorError::InvalidUtf8 {
606        path: path.to_path_buf(),
607    })
608}
609fn now() -> u64 {
610    SystemTime::now()
611        .duration_since(UNIX_EPOCH)
612        .unwrap_or_default()
613        .as_secs()
614}
615fn xxh32(input: &[u8], seed: u32) -> u32 {
616    const P1: u32 = 0x9E3779B1;
617    const P2: u32 = 0x85EBCA77;
618    const P3: u32 = 0xC2B2AE3D;
619    const P4: u32 = 0x27D4EB2F;
620    const P5: u32 = 0x165667B1;
621    fn r(x: u32, n: u32) -> u32 {
622        x.rotate_left(n)
623    }
624    let mut h = seed.wrapping_add(P5).wrapping_add(input.len() as u32);
625    let mut i = 0;
626    while i + 4 <= input.len() {
627        h = h
628            .wrapping_add(u32::from_le_bytes(input[i..i + 4].try_into().unwrap()).wrapping_mul(P3));
629        h = r(h, 17).wrapping_mul(P4);
630        i += 4;
631    }
632    while i < input.len() {
633        h = h.wrapping_add((input[i] as u32).wrapping_mul(P5));
634        h = r(h, 11).wrapping_mul(P1);
635        i += 1;
636    }
637    h ^= h >> 15;
638    h = h.wrapping_mul(P2);
639    h ^= h >> 13;
640    h = h.wrapping_mul(P3);
641    h ^ (h >> 16)
642}