tensorfs 0.3.26__tar.gz → 0.3.28__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {tensorfs-0.3.26 → tensorfs-0.3.28}/Cargo.lock +2 -2
  2. {tensorfs-0.3.26 → tensorfs-0.3.28}/Cargo.toml +1 -1
  3. {tensorfs-0.3.26 → tensorfs-0.3.28}/PKG-INFO +1 -1
  4. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/providers.rs +475 -1
  5. tensorfs-0.3.28/crates/tensorfs-core/tests/instrument_profiles.rs +213 -0
  6. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-py/src/store.rs +24 -3
  7. {tensorfs-0.3.26 → tensorfs-0.3.28}/pyproject.toml +1 -1
  8. {tensorfs-0.3.26 → tensorfs-0.3.28}/python/tensorfs/_ext.pyi +1 -1
  9. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/Cargo.toml +0 -0
  10. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/build.rs +0 -0
  11. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  12. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  13. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  14. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  15. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/b64.rs +0 -0
  16. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  17. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  18. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  19. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  20. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  21. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  22. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  23. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  24. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  25. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/canon.rs +0 -0
  26. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/capability.rs +0 -0
  27. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/catalog.rs +0 -0
  28. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/cbor.rs +0 -0
  29. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  30. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/corpus.rs +0 -0
  31. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  32. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  33. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  34. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  35. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  36. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  37. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/derived.rs +0 -0
  38. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/dtype.rs +0 -0
  39. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/durability.rs +0 -0
  40. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/err.rs +0 -0
  41. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/fetch.rs +0 -0
  42. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/fill.rs +0 -0
  43. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/fit.rs +0 -0
  44. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/gc.rs +0 -0
  45. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/header.rs +0 -0
  46. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/home.rs +0 -0
  47. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ids.rs +0 -0
  48. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  49. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  50. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  51. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  52. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  53. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  54. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  55. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  56. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  57. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  58. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/jcs.rs +0 -0
  59. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/lib.rs +0 -0
  60. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/limits.rs +0 -0
  61. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/machine.rs +0 -0
  62. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/manifest.rs +0 -0
  63. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/meta.rs +0 -0
  64. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/project.rs +0 -0
  65. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/read.rs +0 -0
  66. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/receipt.rs +0 -0
  67. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/reclaim.rs +0 -0
  68. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/registry.rs +0 -0
  69. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/relation.rs +0 -0
  70. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  71. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/repository.rs +0 -0
  72. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/sha256.rs +0 -0
  73. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  74. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/spec.rs +0 -0
  75. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/staging.rs +0 -0
  76. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/storage.rs +0 -0
  77. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/store.rs +0 -0
  78. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/api.rs +0 -0
  79. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/http.rs +0 -0
  80. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  81. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  82. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  83. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  84. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  85. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  86. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/push.rs +0 -0
  87. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  88. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  89. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/src/vectors.rs +0 -0
  90. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  91. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fit.rs +0 -0
  92. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  93. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  94. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  95. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  96. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  97. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  98. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  99. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  100. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/home.rs +0 -0
  101. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  102. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/liveness.rs +0 -0
  103. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  104. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  105. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  106. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  107. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  108. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  109. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  110. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  111. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  112. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  113. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  114. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  115. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/staging.rs +0 -0
  116. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  117. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  118. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/version.rs +0 -0
  119. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-core/tests/write_through.rs +0 -0
  120. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-py/Cargo.toml +0 -0
  121. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-py/src/docs.rs +0 -0
  122. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-py/src/errors.rs +0 -0
  123. {tensorfs-0.3.26 → tensorfs-0.3.28}/crates/tensorfs-py/src/lib.rs +0 -0
  124. {tensorfs-0.3.26 → tensorfs-0.3.28}/python/tensorfs/__init__.py +0 -0
  125. {tensorfs-0.3.26 → tensorfs-0.3.28}/python/tensorfs/errors.py +0 -0
  126. {tensorfs-0.3.26 → tensorfs-0.3.28}/python/tensorfs/py.typed +0 -0
@@ -392,7 +392,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
392
392
 
393
393
  [[package]]
394
394
  name = "tensorfs-core"
395
- version = "0.3.26"
395
+ version = "0.3.28"
396
396
  dependencies = [
397
397
  "fs2",
398
398
  "minicbor",
@@ -404,7 +404,7 @@ dependencies = [
404
404
 
405
405
  [[package]]
406
406
  name = "tensorfs-py"
407
- version = "0.3.26"
407
+ version = "0.3.28"
408
408
  dependencies = [
409
409
  "pyo3",
410
410
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.26"
6
+ version = "0.3.28"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.26
3
+ Version: 0.3.28
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -30,6 +30,8 @@ const API_PAGE_MAX: usize = 64;
30
30
  /// A member the provider does not identify by digest is fetched whole and hashed; the
31
31
  /// class is HuggingFace non-LFS files (configs, tokenizers), which git itself keeps small.
32
32
  const UNPINNED_MEMBER_MAX_BYTES: u64 = 64 << 20;
33
+ /// An explicit ordinary-file selection is a bounded working set, not a weight download.
34
+ const FILE_SELECTION_MAX_BYTES: u64 = 64 << 20;
33
35
  const MEMBER_MAX: usize = 4096;
34
36
  /// A `*.safetensors.index.json` is a map from tensor name to shard file. It is JSON that
35
37
  /// git itself keeps small, and the Go resolver bounded it at the same 32 MiB.
@@ -250,6 +252,17 @@ fn list_observed(
250
252
  credentials: &dyn CredentialProvider,
251
253
  deadline: Deadline,
252
254
  ledger: &Ledger,
255
+ ) -> Result<Vec<SourceMember>> {
256
+ list_selected_observed(uri, endpoints, credentials, deadline, ledger, None)
257
+ }
258
+
259
+ fn list_selected_observed(
260
+ uri: &SourceUri,
261
+ endpoints: &Endpoints,
262
+ credentials: &dyn CredentialProvider,
263
+ deadline: Deadline,
264
+ ledger: &Ledger,
265
+ files: Option<&[String]>,
253
266
  ) -> Result<Vec<SourceMember>> {
254
267
  let policy = endpoints.policy(uri);
255
268
  let base = endpoints.base(uri);
@@ -287,6 +300,9 @@ fn list_observed(
287
300
  let Some(path) = json_str(row, "path") else {
288
301
  return refuse(Code::MISSING_FIELD, "a tree row names no path");
289
302
  };
303
+ if files.is_some_and(|files| !files.iter().any(|file| file == path)) {
304
+ continue;
305
+ }
290
306
  if !member_path(path) {
291
307
  // Every HuggingFace repository carries `.gitattributes`, and a
292
308
  // leading dot is outside the member grammar. Refusing the whole
@@ -345,7 +361,10 @@ fn list_observed(
345
361
  member: path.to_string(),
346
362
  length,
347
363
  sha256,
348
- url: format!("{base}/{org}/{repo}/resolve/{revision}/{path}"),
364
+ url: format!(
365
+ "{base}/{org}/{repo}/resolve/{revision}/{}",
366
+ path.replace(' ', "%20")
367
+ ),
349
368
  primary: false,
350
369
  });
351
370
  }
@@ -1342,6 +1361,145 @@ pub fn resolve(
1342
1361
  })
1343
1362
  }
1344
1363
 
1364
+ /// Resolve only explicitly named ordinary files at one immutable HF revision.
1365
+ /// No suffix has special meaning here: selecting Python bytes never executes them,
1366
+ /// and selecting an index does not expand it into tensor shards. Tensor carriers
1367
+ /// continue to use `resolve` and reviewed profile/carrier selection.
1368
+ pub fn resolve_files(
1369
+ uri: &SourceUri,
1370
+ files: &[String],
1371
+ endpoints: &Endpoints,
1372
+ credentials: &dyn CredentialProvider,
1373
+ deadline: Deadline,
1374
+ ) -> Result<Resolution> {
1375
+ let SourceUri::HuggingFace {
1376
+ org,
1377
+ repo,
1378
+ revision,
1379
+ } = uri
1380
+ else {
1381
+ return refuse(
1382
+ Code::SOURCE_NOT_ALLOWED,
1383
+ "ordinary file selection requires HuggingFace",
1384
+ );
1385
+ };
1386
+ if files.is_empty() || files.len() > MEMBER_MAX {
1387
+ return refuse(
1388
+ Code::COUNT_CAP,
1389
+ "ordinary file selection count is outside the bound",
1390
+ );
1391
+ }
1392
+ let mut names = files.to_vec();
1393
+ names.sort();
1394
+ if names
1395
+ .iter()
1396
+ .any(|name| !member_path(name) || crate::manifest::check_path(name).is_err())
1397
+ {
1398
+ return refuse(
1399
+ Code::PATH_ILLEGAL,
1400
+ "ordinary file selection requires exact member paths",
1401
+ );
1402
+ }
1403
+ if names.windows(2).any(|pair| pair[0] == pair[1]) {
1404
+ return refuse(
1405
+ Code::DUPLICATE_KEY,
1406
+ "ordinary file selection names one member twice",
1407
+ );
1408
+ }
1409
+ let ledger = Ledger::new();
1410
+ let policy = endpoints.policy(uri);
1411
+ let listed =
1412
+ list_selected_observed(uri, endpoints, credentials, deadline, &ledger, Some(&names))?;
1413
+ // Establish the whole roster before fetching any body. A missing later member
1414
+ // must not turn a partial requested tree into a successful smaller selection.
1415
+ let selected = names
1416
+ .iter()
1417
+ .map(|name| {
1418
+ listed
1419
+ .iter()
1420
+ .find(|member| &member.member == name)
1421
+ .ok_or_else(|| Refusal {
1422
+ code: Code::MISSING_FIELD,
1423
+ detail: format!("the source does not contain requested file {name:?}"),
1424
+ })
1425
+ })
1426
+ .collect::<Result<Vec<_>>>()?;
1427
+ let listed_bytes = selected.iter().try_fold(0u64, |total, member| {
1428
+ total.checked_add(member.length.unwrap_or(0))
1429
+ });
1430
+ if listed_bytes.is_none_or(|bytes| bytes > FILE_SELECTION_MAX_BYTES) {
1431
+ return refuse(
1432
+ Code::SIZE_CAP,
1433
+ "ordinary file selection exceeds its byte bound",
1434
+ );
1435
+ }
1436
+ let mut remaining = FILE_SELECTION_MAX_BYTES;
1437
+ let mut resolved = Vec::with_capacity(selected.len());
1438
+ for member in selected {
1439
+ if member
1440
+ .length
1441
+ .map_or(remaining == 0, |length| length > remaining)
1442
+ {
1443
+ return refuse(
1444
+ Code::SIZE_CAP,
1445
+ "ordinary file selection exceeds its byte bound",
1446
+ );
1447
+ }
1448
+ let (object, provenance) = if let Some(sha256) = &member.sha256 {
1449
+ let length = match member.length {
1450
+ Some(length) => length,
1451
+ None => probe_length(&member.url, &policy, credentials, deadline, &ledger)?,
1452
+ };
1453
+ (
1454
+ ObjectRef {
1455
+ sha256: sha256.clone(),
1456
+ length,
1457
+ },
1458
+ Provenance::Declared,
1459
+ )
1460
+ } else {
1461
+ let (body, _) = api_get(
1462
+ &member.url,
1463
+ &policy,
1464
+ credentials,
1465
+ remaining,
1466
+ deadline,
1467
+ &ledger,
1468
+ )?;
1469
+ if member
1470
+ .length
1471
+ .is_some_and(|length| length != body.len() as u64)
1472
+ {
1473
+ return refuse(
1474
+ Code::LENGTH_MISMATCH,
1475
+ "ordinary file body disagrees with listed length",
1476
+ );
1477
+ }
1478
+ (ObjectRef::of(&body), Provenance::Computed)
1479
+ };
1480
+ remaining = remaining
1481
+ .checked_sub(object.length)
1482
+ .ok_or_else(|| Refusal {
1483
+ code: Code::SIZE_CAP,
1484
+ detail: "ordinary file selection exceeds its byte bound".into(),
1485
+ })?;
1486
+ resolved.push(ResolvedMember {
1487
+ member: member.member.clone(),
1488
+ object,
1489
+ url: member.url.clone(),
1490
+ provenance,
1491
+ carrier: false,
1492
+ requires: Vec::new(),
1493
+ });
1494
+ }
1495
+ let canonical = format!("hf://{org}/{repo}@{revision}");
1496
+ Ok(Resolution {
1497
+ selection_sha256: selection_digest(&canonical, &resolved),
1498
+ canonical,
1499
+ members: resolved,
1500
+ })
1501
+ }
1502
+
1345
1503
  #[cfg(test)]
1346
1504
  mod tests {
1347
1505
  use super::*;
@@ -2231,4 +2389,320 @@ mod tests {
2231
2389
  handle.join().unwrap();
2232
2390
  let _ = std::fs::remove_dir_all(root);
2233
2391
  }
2392
+
2393
+ #[test]
2394
+ fn explicit_files_resolve_only_named_bytes_and_retain_across_restart() {
2395
+ let config = b"{\"model_type\":\"test\"}".to_vec();
2396
+ let tokenizer = b"these are tokenizer bytes".to_vec();
2397
+ let expected = vec![ObjectRef::of(&config), ObjectRef::of(&tokenizer)];
2398
+ let listing = format!(
2399
+ r#"[
2400
+ {{"type":"file","path":"model.safetensors","size":100000000000,"lfs":{{"oid":"{}","size":100000000000}}}},
2401
+ {{"type":"file","path":"config.json","size":{}}},
2402
+ {{"type":"file","path":"tokenizer files/tokenizer.json","size":{}}},
2403
+ {{"type":"file","path":"unused.py","size":99}}
2404
+ ]"#,
2405
+ "*".repeat(64),
2406
+ config.len(),
2407
+ tokenizer.len()
2408
+ );
2409
+ // Two order-independent resolutions each read list + two small bodies;
2410
+ // materialization reads the bodies once, and restart/warm admission reads none.
2411
+ let (base, seen, handle) = origin(8, move |request| {
2412
+ if request.path.contains("/tree/") {
2413
+ ok_body(listing.as_bytes(), "application/json")
2414
+ } else if request.path.ends_with("/config.json") {
2415
+ ok_body(&config, "application/json")
2416
+ } else if request.path.ends_with("/tokenizer.json") {
2417
+ ok_body(&tokenizer, "application/json")
2418
+ } else {
2419
+ panic!("unselected file requested: {}", request.path);
2420
+ }
2421
+ });
2422
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2423
+ let endpoints = Endpoints {
2424
+ huggingface: base,
2425
+ allow_local: true,
2426
+ ..Endpoints::default()
2427
+ };
2428
+ let mut names = vec![
2429
+ "config.json".into(),
2430
+ "tokenizer files/tokenizer.json".into(),
2431
+ ];
2432
+ let selected =
2433
+ resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none()).unwrap();
2434
+ names.reverse();
2435
+ assert_eq!(
2436
+ selected,
2437
+ resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none()).unwrap()
2438
+ );
2439
+ assert_eq!(
2440
+ selected
2441
+ .members
2442
+ .iter()
2443
+ .map(|m| m.object.clone())
2444
+ .collect::<Vec<_>>(),
2445
+ expected
2446
+ );
2447
+ assert!(selected
2448
+ .members
2449
+ .iter()
2450
+ .all(|m| m.provenance == Provenance::Computed && !m.carrier && m.requires.is_empty()));
2451
+ let root = temporary("selected-files");
2452
+ let store = Store::init(&root).unwrap();
2453
+ let producer = ObjectRef::of(b"files-producer").id();
2454
+ let recipient = ObjectRef::of(b"files-recipient").id();
2455
+ let (cold, transfers) = materialize_selected(
2456
+ &store,
2457
+ &producer,
2458
+ &selected,
2459
+ &endpoints.policy(&uri),
2460
+ &Anonymous,
2461
+ Deadline::none(),
2462
+ &mut |_, _| {},
2463
+ )
2464
+ .unwrap();
2465
+ assert_eq!(
2466
+ transfers.iter().map(|row| row.transferred).sum::<u64>(),
2467
+ selected.member_bytes()
2468
+ );
2469
+ crate::source_artifact::retain(&store, &producer, &recipient).unwrap();
2470
+ drop(store);
2471
+ let store = Store::open(&root).unwrap();
2472
+ let (warm, transfers) = materialize_selected(
2473
+ &store,
2474
+ &producer,
2475
+ &selected,
2476
+ &endpoints.policy(&uri),
2477
+ &Anonymous,
2478
+ Deadline::none(),
2479
+ &mut |_, _| {},
2480
+ )
2481
+ .unwrap();
2482
+ assert_eq!(warm, cold);
2483
+ assert!(transfers.iter().all(|row| row.held && row.transferred == 0));
2484
+ crate::source_artifact::release(&store, &producer).unwrap();
2485
+ crate::gc::collect(&root, false).unwrap();
2486
+ let retained = crate::source_artifact::read(&store, &recipient)
2487
+ .unwrap()
2488
+ .unwrap();
2489
+ assert!(retained.complete && !retained.released);
2490
+ for object in &expected {
2491
+ assert!(store.record_valid(&object.sha256).is_ok());
2492
+ }
2493
+ crate::source_artifact::release(&store, &recipient).unwrap();
2494
+ crate::gc::collect(&root, false).unwrap();
2495
+ for object in expected {
2496
+ assert!(!store.contains(&object.sha256));
2497
+ }
2498
+ handle.join().unwrap();
2499
+ assert_eq!(seen.lock().unwrap().len(), 8);
2500
+ assert!(seen
2501
+ .lock()
2502
+ .unwrap()
2503
+ .iter()
2504
+ .any(|(path, _)| path.contains("tokenizer%20files/")));
2505
+ std::fs::remove_dir_all(root).unwrap();
2506
+ }
2507
+
2508
+ #[test]
2509
+ fn explicit_files_refuse_missing_and_over_budget_before_fetching_bodies() {
2510
+ for (names, listing, code) in [
2511
+ (
2512
+ vec!["absent.json".into(), "config.json".into()],
2513
+ r#"[{"type":"file","path":"config.json","size":2}]"#.to_string(),
2514
+ Code::MISSING_FIELD,
2515
+ ),
2516
+ (
2517
+ vec!["config.json".into()],
2518
+ format!(
2519
+ r#"[{{"type":"file","path":"config.json","size":{}}}]"#,
2520
+ FILE_SELECTION_MAX_BYTES + 1
2521
+ ),
2522
+ Code::SIZE_CAP,
2523
+ ),
2524
+ ] {
2525
+ let (base, _, handle) =
2526
+ origin(1, move |_| ok_body(listing.as_bytes(), "application/json"));
2527
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2528
+ let endpoints = Endpoints {
2529
+ huggingface: base,
2530
+ allow_local: true,
2531
+ ..Endpoints::default()
2532
+ };
2533
+ assert_eq!(
2534
+ resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none())
2535
+ .unwrap_err()
2536
+ .code,
2537
+ code
2538
+ );
2539
+ handle.join().unwrap();
2540
+ }
2541
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2542
+ for names in [
2543
+ vec!["../config.json".into()],
2544
+ vec!["*.json".into()],
2545
+ vec!["/config.json".into()],
2546
+ ] {
2547
+ assert_eq!(
2548
+ resolve_files(
2549
+ &uri,
2550
+ &names,
2551
+ &Endpoints::default(),
2552
+ &Anonymous,
2553
+ Deadline::none()
2554
+ )
2555
+ .unwrap_err()
2556
+ .code,
2557
+ Code::PATH_ILLEGAL
2558
+ );
2559
+ }
2560
+ assert_eq!(
2561
+ resolve_files(
2562
+ &uri,
2563
+ &["config.json".into(), "config.json".into()],
2564
+ &Endpoints::default(),
2565
+ &Anonymous,
2566
+ Deadline::none()
2567
+ )
2568
+ .unwrap_err()
2569
+ .code,
2570
+ Code::DUPLICATE_KEY
2571
+ );
2572
+ }
2573
+
2574
+ #[test]
2575
+ fn explicit_files_refuse_a_changed_listing_length() {
2576
+ let (base, _, handle) = origin(2, move |request| {
2577
+ if request.path.contains("/tree/") {
2578
+ ok_body(
2579
+ br#"[{"type":"file","path":"config.json","size":2}]"#,
2580
+ "application/json",
2581
+ )
2582
+ } else {
2583
+ ok_body(b"changed", "application/json")
2584
+ }
2585
+ });
2586
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2587
+ let endpoints = Endpoints {
2588
+ huggingface: base,
2589
+ allow_local: true,
2590
+ ..Endpoints::default()
2591
+ };
2592
+ assert_eq!(
2593
+ resolve_files(
2594
+ &uri,
2595
+ &["config.json".into()],
2596
+ &endpoints,
2597
+ &Anonymous,
2598
+ Deadline::none()
2599
+ )
2600
+ .unwrap_err()
2601
+ .code,
2602
+ Code::LENGTH_MISMATCH
2603
+ );
2604
+ handle.join().unwrap();
2605
+ }
2606
+
2607
+ #[test]
2608
+ fn explicit_files_changed_after_resolution_never_finish_a_native_root() {
2609
+ let replies = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2610
+ let (base, _, handle) = origin(3, move |request| {
2611
+ if request.path.contains("/tree/") {
2612
+ ok_body(
2613
+ br#"[{"type":"file","path":"config.json","size":2}]"#,
2614
+ "application/json",
2615
+ )
2616
+ } else {
2617
+ let attempt = replies.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2618
+ ok_body(if attempt == 0 { b"AA" } else { b"BB" }, "application/json")
2619
+ }
2620
+ });
2621
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2622
+ let endpoints = Endpoints {
2623
+ huggingface: base,
2624
+ allow_local: true,
2625
+ ..Endpoints::default()
2626
+ };
2627
+ let selected = resolve_files(
2628
+ &uri,
2629
+ &["config.json".into()],
2630
+ &endpoints,
2631
+ &Anonymous,
2632
+ Deadline::none(),
2633
+ )
2634
+ .unwrap();
2635
+ let root = temporary("changed-files");
2636
+ let store = Store::init(&root).unwrap();
2637
+ let producer = ObjectRef::of(b"changed-files-producer").id();
2638
+ let error = materialize_selected(
2639
+ &store,
2640
+ &producer,
2641
+ &selected,
2642
+ &endpoints.policy(&uri),
2643
+ &Anonymous,
2644
+ Deadline::none(),
2645
+ &mut |_, _| {},
2646
+ )
2647
+ .unwrap_err();
2648
+ assert_eq!(error.code, Code::OBJECT_ID_MISMATCH);
2649
+ assert!(!store.contains(&selected.members[0].object.sha256));
2650
+ assert!(
2651
+ !crate::source_artifact::read(&store, &producer)
2652
+ .unwrap()
2653
+ .unwrap()
2654
+ .complete
2655
+ );
2656
+ handle.join().unwrap();
2657
+ std::fs::remove_dir_all(root).unwrap();
2658
+ }
2659
+
2660
+ #[test]
2661
+ fn explicit_files_can_retain_an_empty_file() {
2662
+ let (base, _, handle) = origin(2, move |request| {
2663
+ if request.path.contains("/tree/") {
2664
+ ok_body(
2665
+ br#"[{"type":"file","path":"empty.py","size":0}]"#,
2666
+ "application/json",
2667
+ )
2668
+ } else {
2669
+ ok_body(b"", "text/plain")
2670
+ }
2671
+ });
2672
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2673
+ let endpoints = Endpoints {
2674
+ huggingface: base,
2675
+ allow_local: true,
2676
+ ..Endpoints::default()
2677
+ };
2678
+ let selected = resolve_files(
2679
+ &uri,
2680
+ &["empty.py".into()],
2681
+ &endpoints,
2682
+ &Anonymous,
2683
+ Deadline::none(),
2684
+ )
2685
+ .unwrap();
2686
+ assert_eq!(selected.members[0].object, ObjectRef::of(b""));
2687
+ let root = temporary("empty-files");
2688
+ let store = Store::init(&root).unwrap();
2689
+ let producer = ObjectRef::of(b"empty-files-producer").id();
2690
+ let (retained, transfers) = materialize_selected(
2691
+ &store,
2692
+ &producer,
2693
+ &selected,
2694
+ &endpoints.policy(&uri),
2695
+ &Anonymous,
2696
+ Deadline::none(),
2697
+ &mut |_, _| {},
2698
+ )
2699
+ .unwrap();
2700
+ assert!(retained.complete && !retained.released);
2701
+ assert_eq!(transfers[0].transferred, 0);
2702
+ assert!(store
2703
+ .record_valid(&selected.members[0].object.sha256)
2704
+ .is_ok());
2705
+ handle.join().unwrap();
2706
+ std::fs::remove_dir_all(root).unwrap();
2707
+ }
2234
2708
  }