Skip to content
palmoni5 edited this page Sep 20, 2026 · 2 revisions

מסדים מצורפים

מסד מצורף הוא קובץ SQLite בסכמת ספריית אוצריא (כמו seforim.db) שהמשתמש מצרף לתוכנה. הספרים שבו מקבלים את כל היכולות של ספרים רשמיים ואישיים, המסד נפתח לקריאה בלבד בתצורה מוקשחת, ואף טבלה אינה חובה.

תיעוד המשתמש ובוני המסדים: מסדי ספרים אישיים ו־docs/personal_databases.md.

זהות ומפתחות

BookSource (lib/models/book_source.dart) הוא מקור האמת לזהות הספר, ומחליף את isUserBook בכל השכבות:

שכבה ערכים
BookSource official / user / attached(slug)
מפתח wire (תוספים, JSON) o / u / d:<slug>
BookCompositeKey סיומת מקור על מפתח הספר
מפתח אינדקס החיפוש id: / uid: / ext: / db:<slug>:<id>
הערות אישיות "<title>|db:<slug>"
bookUid לתוספים db:<slug>:<id> עם source: "attached"
  • ה־slug נלקח מ־schema_meta.library_id, ואם אין — משם הקובץ אחרי ניקוי. אותיות (כולל עברית), ספרות, ._-, עד 64 תווים. התנגשות slug ← הקובץ השני נדחה.
  • המפתחות של ספרים רשמיים ואישיים לא השתנו — סימניות, היסטוריה, אינדקס והגדרות פר־ספר שרדו את המעבר.
  • ספרים מצורפים נוספים בסוף סדר הקטלוג של האינדקס, כך שצירוף מסד לא מזיז מפתחות של ספרים אחרים ולא מאנדקס אותם מחדש. החלפת קובץ באותו slug ← אינדוקס מחדש של אותו מסד בלבד.

מבנה הקוד

lib/attached_libraries/
├── bloc/          # מצב המסדים בהגדרות (צירוף, הסרה, עדיפות, מיקום, הסתרה)
├── models/
├── repository/
│   ├── registry            # פתיחה עצלה ומוקשחת, סגירה בזמן סרק, closeAll
│   ├── probe (isolate)     # בדיקת קובץ לפני צירוף: slug, WAL/journal, טבלאות
│   ├── store               # שמירת רשימת המסדים והגדרותיהם
│   ├── external_link_core / external_link_repository
│   └── update/             # מקור עדכונים, מניפסט, חתימה, רשת, התקנה
└── view/
lib/migration/database/
├── db_capabilities.dart    # מקור אמת יחיד לקיום טבלאות/עמודות
└── untrusted_database.dart # openUntrustedReadOnlyDatabase, ReadOnlyDbTarget
  • DbCapabilities — רשימת היתר של טבלאות + getter יכולת לכל טבלה/זוג, במטמון לפי נתיב + PRAGMA schema_version. שם מוכר שהוא VIEW או טבלה וירטואלית נחשב חסר. זה חל גם על seforim.db הרשמי: טבלה חסרה משביתה רק את התכונה שתלויה בה.
  • openUntrustedReadOnlyDatabase — read-only, query_only, trusted_schema=OFF, מצב defensive, ללא הרחבות, mmap כבוי. ReadOnlyDbTarget מעביר את אותה פתיחה ל־isolates.
  • קטלוג המסדים נקרא ב־isolate עם timeout לכל מסד; מסד שלא עונה מסומן "לא זמין" והעץ ממשיך להיבנות.
  • איתור מקורות במסדים מצורפים רץ ב־isolate עובד ברקע.

קישורים חוצי־מסדים — external_link

טבלה אופציונלית שמאפשרת לספר במסד מצורף להצביע על ספר רשמי או על מסד אחר, לפי כותרת ו־heRef (לא FK), כדי לשרוד עדכוני ספרייה. ל־connectionType אותה משמעות כמו בטבלת link הרשמית — היעד כפי שהוא נראה מהמקור; ולכן פירוש שרוצה להופיע בחלונית המפרשים של ספר רשמי כותב connectionType='SOURCE'.

הקישורים ההפוכים מוגשים מאינדקס צד ב־cache.db, שנבנה מחדש כשקובץ המסד או גרסת הספרייה הרשמית משתנים. מגבלה: 5,000,000 שורות למסד (הבנייה בזרם, במנות), וחיתוך כותרת/הפניה ב־512 תווים.

גרסאות: ספר אישי כגרסה של ספר אחר

user_book_version תמך עד כה רק בצמד אישי↔אישי. כעת הספר הראשי יכול להיות גם ספר רשמי או ספר במסד מצורף, לפי העמודות מקור_ראשי ו־קטגוריית_ראשי בקובץ גרסאות.csv. הראשי מצוין תמיד לפי כותרת כדי לשרוד עדכוני ספרייה; קבצים ישנים ממשיכים לעבוד בלי שינוי.

עלייה

_runDeferredAttachedLibraries ב־main.dart רץ אחרי חשיפת החלון (שער reveal עם timeout), מוגן ב־WindowRole.isSecondary, ונכשל ללא הפלה דרך _logNonFatalInitializationError. שום דבר מהפיצ'ר לא נוסף למסלול שלפני הפריים הראשון — ראו רצף האתחול. כשאין מסדים מצורפים אין שום עלות בפתיחת ספר רשמי.

כלל היתומים באינדקס

מפתחות db: נמחקים מהאינדקס רק כאשר:

  1. המסד הוסר מהרשימה, או
  2. הספר נעלם ממסד שנטען, גלוי ונגיש.

מסד לא זמין או מוסתר לעולם אינו גורם למחיקה — אחרת ניתוק כונן היה מוחק את האינדקס, הסימניות וההערות.

הוספת תמיכה בטבלה חדשה

  1. הוסיפו את שם הטבלה לרשימת ההיתר ב־DbCapabilities (וגם את בת הזוג, אם הן חייבות לבוא יחד).
  2. הוסיפו getter יכולתhasX שבודק קיום ועמודות נדרשות.
  3. שער בנקודת השאילתה — כל שאילתה נשמרת ב־getter ומחזירה ריק כשהיכולת חסרה. אין להניח קיום טבלה.
  4. עדכנו את tool/validate_personal_db.dart ואת קטלוג הטבלאות ב־docs/personal_databases.md.

טבלה שאינה ברשימת ההיתר לעולם לא נקראת — זה חלק ממודל האבטחה, לא מגבלה זמנית.

אבטחה — עקרונות שאסור לשבור

  • המסד לעולם לא נכתב. עותק מנוהל (מובייל) הוא היחיד שה־journal שלו מנורמל.
  • שום דבר מהמסד אינו מותקן או מורץ — תוספים, סקריפטים או גופנים. זו דרישה מפורשת: התקנת תוסף ממסד מיובא היא פריצת אבטחה.
  • קישורים בטקסט הספר — ניווט בלבד.
  • book.filePath — יחסי לתיקיית המסד בלבד; נדחים נתיב מוחלט, אות כונן, UNC, .., רכיב של נקודות/רווחים בלבד, : ותו NUL.

עדכונים

מקור ונעיצה

המסד מצהיר ב־schema_meta על update_manifest_url (https) ועל update_public_key (ed25519, base64). AttachedLibraryProbe קורא אותם, ו־_applyProbe נועץ אותם ב־AttachedLibrary.updateSource בצירוף (TOFU). בבדיקה חוזרת של אותו slug הנעוץ נשמר, וכל סטייה — כולל הסרה של השדות — מסמנת updateSourceMismatch ללא שום פנייה לרשת. אין החלפת מפתח בגרסה הזו: מפתח שאבד מחייב הסרה וצירוף מחדש.

מניפסט וחתימה

  • models/attached_update_manifest.dart — פענוח קפדני רק אחרי אימות החתימה. checkApplicable דוחה library_id שונה ו־db_version שאינו גדול מהמותקן.
  • repository/update/attached_update_signature.dart — ed25519 דרך pinenacl (Dart טהור), תמיד מול המפתח הנעוץ ולעולם לא מול מפתח שהגיע בקובץ שהורד.
  • המניפסט מתאר ארטיפקט מלא: דחיסה, גודל, sha256 של ה־.db הסופי, ו־parts[] — כך שמסד גדול מ־2GB מפוצל לכמה חלקים ויכול לשבת על GitHub Releases או על כל שרת https אחר. לצדו יכולים לשבת תיקוני דלתא (ראו בהמשך).

רשת

AttachedUpdateHostPolicy דורשת https, שם מארח ולא IP, בלי שמות מקומיים, וכל כתובות ה־DNS ציבוריות. AttachedUpdateFetcher מתחבר דרך connectionFactory לכתובת שהמדיניות כבר אישרה ומקים TLS מול שם המארח (אין פענוח DNS שני — חסימת rebinding), מטפל בהפניות ידנית ובודק כל אחת, ומחיל timeouts על החיבור ועל כל נתח. תעודות נטפרי מגיעות מה־SecurityContext הגלובלי. dart:io בלבד, כדי שיוכל לרוץ ב־isolate.

מגבלה מתועדת: מאחורי פרוקסי HTTP, הפרוקסי הוא שמפענח את השם, ולכן חסימת הכתובות הפרטיות נשענת על פענוח מקומי בלבד.

תזמון

_runDeferredAttachedLibraryUpdates() רץ אחרי סריקת המסדים, בחלון הראשי בלבד, מאחורי אותם שערים כמו בדיקת הספרייה הרשמית: לא במצב לא מקוון, keySoftwareAndBookUpdatesEnabled, keyAutoSync, ותדירות keyUpdateCheckFrequency. מסד בלי מקור נעוץ או עם updateSourceMismatch לא נבדק כלל.

הורדה והתקנה

  • AttachedUpdateDownloader מריץ הורדה, sha256 לכל חלק, פריסת zstd בזרם (ZstdStreamExtractor, אותו FFI של עדכון הספרייה) ו־sha256 סופי — הכל ב־isolate אחד, עם ביטול דרך פורט בקרה והמשכיות ב־Range אחרי כשל רשת.
  • לפני ההורדה נבדקת כתיבוּת תיקיית היעד ומקום פנוי בכל כונן (מרווח 32MB).
  • installUpdate רץ בתור הסדרתי של המאגר: registry.close(slug) משחרר נעילות, AttachedUpdateFileSwap.swapIn מעביר את הקובץ הישן וקבצי הצד שלו ל־<file>.bak-update ומכניס את החדש. הקובץ החדש נבדק פעמיים — לפני ההחלפה ואחריה: אותו library_id, אותו מפתח, ו־db_version השווה בדיוק למניפסט. כשל ⇒ שחזור הגיבוי.
  • קריסה באמצע: recoverInterruptedUpdates() רץ לפני ה־rescan.bak-update קיים ⇒ מוחזר למקום, וקובץ .update-new יתום נמחק.
  • אחרי הצלחה, טביעת האצבע החדשה מפעילה את מסלול הרענון הקיים: עץ, אינדוקס של אותו מקור בלבד, אינדקס external_link ומטמונים.

כלי למפרסמים

dart run tool/personal_db_update.dart keygen|pack|sign|verify — יצירת מפתח, דחיסה ופיצול לחלקים עם sha256, חתימה ואימות.

תיקוני דלתא

הדלתא כאן היא ברמת הבתים — פלט של zstd --patch-from — ולא patch ברמת השורה כמו בספרייה הרשמית. זו הייתה ההכרעה המרכזית: ה־PatchApplier של seforim_library_updater קשור לרשימת הטבלאות ולמפתחות הראשיים של הסכמה הרשמית, ל־LogicalContentHasher שמחושב על סדר טבלאות קפוא, ולקובצי patch שמיוצרים בצינור ה־Kotlin של SeforimLibrary — למפרסם פרטי אין דרך לייצר אותם, ובמסד שנבנה מחדש בכל גרסה גם ממילא אין זהות שורה יציבה שתעשה את ה־patch קטן.

  • מניפסט: ערך ב־delta[] עם compression: "zstd-patch", ‏from_db_version ו־from_sha256. ה־size וה־sha256 הם של ה־.db המוגמר — זהים לאלה של full, ולכן הערובה הסופית אינה משתנה.
  • סובלנות: ערך דלתא שאינו נפרש מדולג, ולא מפיל את המניפסט כולו. בלי זה, מפרסם שמוסיף דלתא היה משתיק את העדכונים אצל כל לקוח ישן יותר. ‏full נשאר קפדני.
  • בחירה (AttachedUpdateArtifactPlanner): דלתא נבחרת רק כש־from_db_version תואם, ה־sha256 של הקובץ המותקן שווה ל־from_sha256, ההורדה קטנה מזו של full, המערכת 64־ביט והקובץ המותקן עד 2GiB. מבין המתאימות נבחרת הקטנה. ה־sha של הקובץ המותקן מחושב ב־isolate רק כשקיימת מועמדת תואמת־גרסה, ונשמר במטמון לפי (נתיב, גודל, mtime).
  • החלה: MappedFile ממפה את הקובץ המותקן לקריאה בלבד, ו־ZSTD_DCtx_refPrefix מקבל את המיפוי כ־prefix עם windowLogMax=31. המיפוי אינו מתיר שיתוף לכתיבה — קיצור הקובץ תוך כדי מיפוי הוא תקלת עמוד שאי אפשר לתפוס ב־Dart והיא מפילה את התהליך, ולכן עדיף שהמיפוי ייכשל והעדכון ירד במלואו.
  • נפילה חזרה: כשל בתיקון עצמו (פענוח, חריגה מהגודל, sha) מוריד את full באותה ריצה, בלי שגיאה למשתמש. כשל רשת אינו מפיל חזרה — הוא מדווח פעם אחת, וההורדה החלקית נשמרת להמשך.
  • ביטול: הפענוח סינכרוני בתוך Isolate.run, ולכן הודעה ב־SendPort הייתה מגיעה רק אחרי שהוא הסתיים. הביטול עובר בתא זיכרון נייטיב (ZstdCancelFlag) שהלולאה בודקת פעם לכל מנת פלט.
  • כלי למפרסם: pack --delta-from <old.db> (עד 4), ו־verify שמחיל כל תיקון דרך אותו applier שהלקוח מריץ.

דפים קשורים

Clone this wiki locally