שמות מטא־נתונים וחיי קובץ¶
נניח שאנחנו מעתיקים קובץ ומקבלים בדיוק את אותם בייטים, אבל התוכנית שמשתמשת בו מתנהגת אחרת. ייתכן שהעתקנו תוכן בלי הרשאות, ACL או extended attributes שהיו חלק מהחוזה. קובץ אינו רק buffer, וכלי גיבוי או עדכון צריך להחליט אילו תכונות הוא משמר ובאיזה סדר.
כבר ראינו ש־hard links מאפשרים לכמה שמות להתייחס לאותו inode. קישור סמלי הוא אובייקט אחר שמכיל הוראת חיפוש, ולכן העתקת יעד הקישור והעתקת הקישור עצמו הן פעולות שונות. אם גיבוי משטח את כל הקישורים לעותקי תוכן, הוא משנה גם זהות וגם צריכת אחסון, אפילו אם הקריאה מכל נתיב מחזירה כרגע אותם בייטים.
חותמות זמן דורשות פירוש מדויק. זמן שינוי תוכן אינו זמן שינוי metadata, וזמן גישה יכול להיות מושפע ממדיניות mount שמפחיתה עדכונים. זמן יצירה אינו שדה שכל ממשק ומערכת קבצים מספקים באותו אופן. אין להסיק ששעון של קובץ הוא מונה גרסאות חד־ערכי או מנגנון סנכרון.
נחקור extended attribute דרך מתאר שכבר נפתח. הדוגמה הבאה דורשת libc ומערכת קבצים שתומכת במרחב user. היא יוצרת קובץ תרגיל חדש בלבד.
use std::{fs::OpenOptions, io};
use std::os::fd::AsRawFd;
fn read_note(file: &std::fs::File) -> io::Result<Vec<u8>> {
for _ in 0..3 {
let size = unsafe { libc::fgetxattr(file.as_raw_fd(), c"user.course".as_ptr(),
std::ptr::null_mut(), 0) };
if size == -1 { return Err(io::Error::last_os_error()); }
if size > 65536 { return Err(io::Error::other("attribute too large")); }
let mut bytes = vec![0u8; size as usize];
let count = unsafe { libc::fgetxattr(file.as_raw_fd(), c"user.course".as_ptr(),
bytes.as_mut_ptr().cast(), bytes.len()) };
if count >= 0 { bytes.truncate(count as usize); return Ok(bytes); }
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::ERANGE) { return Err(error); }
}
Err(io::Error::other("attribute changed repeatedly"))
}
fn main() -> io::Result<()> {
let file = OpenOptions::new().read(true).write(true)
.create_new(true).open("metadata-lab")?;
let value = b"version=1";
if unsafe { libc::fsetxattr(file.as_raw_fd(), c"user.course".as_ptr(),
value.as_ptr().cast(), value.len(), 0) } == -1 {
return Err(io::Error::last_os_error());
}
assert_eq!(read_note(&file)?, value);
Ok(())
}
הקריאה הראשונה שואלת מה גודל הערך, והשנייה מבקשת את הנתונים. בין הקריאות הערך יכול להשתנות, ולכן טיפלנו ב־ERANGE עם מספר ניסיונות חסום. אין לולאה שמבטיחה להצליח מול כותב שממשיך לשנות את הערך לנצח. הגבלת הגודל מונעת מהמטא־נתון החיצוני להפוך לבקשת הקצאה בלתי מוגבלת.
אם הגודל היה אפס ובינתיים גדל, קריאה שנייה עם גודל אפס משמשת שוב כשאילתת גודל. לכן יש לטפל גם בתוצאה חיובית שגדולה מהבאפר ולא להציג אותה כתוכן שקראנו. בתרגול נשנה את הפונקציה כך שתבדוק זאת במפורש ותנסה שוב, משום שגם קצה שנראה נדיר הוא חלק מהחוזה.
הערך הוא רצף בייטים, ולא בהכרח UTF-8 או מחרוזת שמסתיימת באפס. שם התכונה משתמש במוסכמת הממשק של סי, אבל תוכן התכונה מקבל אורך מפורש. אל תבצעו CStr::from_ptr על תוכן שרירותי רק משום שהוא הגיע מפונקציה זרה.
הרשאות mode ו־ACL יכולות להשתלב כך שההרשאה האפקטיבית שונה מקריאה שטחית של שלוש ספרות. מסכה של ACL יכולה להגביל הרשאות של רשומות מסוימות, ובעלות וקבוצות משפיעות על בחירת הרשומה. כלי העתקה צריך גם להחליט אם יש לו הרשאה לשמר בעלות ומרחבי תכונות כגון security, ולא להתעלם מכשל כזה כאילו ההעתקה הושלמה במלואה.
סדר העדכון יכול לשנות תוצאה. כתיבת תוכן חדש יכולה לעדכן זמנים, שינוי בעלות יכול להשפיע על ביטים מסוימים, והחלפת קובץ באמצעות rename מחליפה inode במקום לשנות רק תוכן. לכן בפרוטוקול עדכון נגדיר אם ההרשאות והתכונות מגיעות מהקובץ הישן, מהקובץ החדש או ממדיניות המוצר.
גם מחיקה דרך unlink אינה פקודת מחיקת כל העותקים הפיזיים מכל שכבה. היא מסירה שם לפי חוזה מערכת הקבצים, ואינה הבטחה למחיקה מאובטחת מול snapshots, גיבויים או התקנים עם תרגום פנימי. כאן אנחנו מנתחים חיי אובייקט ושמות, ולא מבטיחים תכונות אחסון שהממשק אינו נותן.
החוזים של תכונות מורחבות נמצאים בעמוד xattr, ושל הרשאות מורחבות בעמוד acl. תיעוד כלי העתקה צריך לומר אילו תכונות הועתקו ואילו נכשלו, ולא להסתפק במספר הבייטים שנכתבו.