| پروژهٔ اولیهٔ این سوال را میتوانید از [این لینک](/contest/assignments/103145/download_problem_initial_project/356755/) دانلود کنید. |
| :-: |
در **جام جهانی فناوری پردیس ۲۰۲۶** آنقدر خبرنگار و بازیکن و داور و مهمان ویژه در ورزشگاه رفتوآمد دارند که دیگر نمیشود دستی کنترل کرد چه کسی اجازهٔ ورود به کدام قسمت را دارد. بهجای دفترچه و مهر و امضا، قرار است یک **ابزار خط فرمان** ساخته شود که کارتها را صادر کند، ماتریس دسترسی را نگه دارد و در لحظهٔ ورود تصمیم بگیرد که این کارت اجازهٔ ورود به موقعیت مشخص را دارد یا نه.
این ابزار باید روی یک سرور خالیِ اتاق عملیات کار کند: نقطهٔ ورود و منطق دستورها با *Bash* و بدون هیچ بستهٔ نصبی از بیرون. سرور فقط چیزهایی را دارد که روی هر ماشین لینوکسی هست و کار شما ساختن این ابزار با همان چیزهاست. کل وضعیت رویداد در یک **دیتابیس SQLite** ذخیره میشود تا چند گیت ورودی بتوانند همزمان به آن دست بزنند بدون اینکه ظرفیت موقعیتها دوبارهشماری شود. اسم این ابزار `accred` است و شما باید آن را از پایه بنویسید!

+ **این سوال، سوالی بسیار سخت و با پیادهسازی مفصل و وقتگیر است!** لازم نیست همهچیز را پیادهسازی کنید. هر گروه از دستورها امتیاز مستقل خودش را دارد و میتوانید از هرجا که راحتترید شروع کنید. جدول «سابتسکها» در انتهای سوال نشان میدهد هر بخش چند درصد از امتیاز سوال را دارد.
# **پروژهٔ اولیه**
برای دانلود **پروژهٔ اولیه** روی [این لینک](/contest/assignments/103145/download_problem_initial_project/356755/) کلیک کنید. برخلاف سؤالهای قبلی، پروژه اولیه این سؤال یک تکفایل نیست؛ یک **درخت پروژهٔ چندفایلی** است که خودتان میسازید: یک نقطهٔ ورود `solution.sh` که دستور را میگیرد و به ماژول درست میسپارد، بههمراه چند ماژول کمکی در پوشهٔ `lib/`.
<details class="grey">
<summary>**نکته: یک چیدمان پیشنهادی برای ماژولها**</summary>
نام و مرزبندی ماژولها کاملاً دلخواه شماست. آنچه سنجیده میشود **رفتار دستورهاست**، نه نام فایلها. درخت زیر فقط یک پیشنهاد است:
```
.
├── <mark class="green" title="فایل اصلی و نقطه ورود برنامه؛ این فایل را تکمیل کنید.">solution.sh</mark>
└── lib/
├── <mark class="green" title="توابع کمکی شامل کدهای خروج، اعتبارسنجی آرگومانها و logging؛ این فایل را تکمیل کنید.">util.sh</mark>
├── <mark class="green" title="لایه دسترسی به دیتابیس SQLite که عملیات را با flock بهصورت سریالی انجام میدهد؛ این فایل را تکمیل کنید.">db.sh</mark>
├── <mark class="green" title="تعریف ساختار و جداول دیتابیس SQLite؛ این فایل را تکمیل کنید.">schema.sql</mark>
├── <mark class="green" title="مدیریت افراد و zoneها؛ این فایل را تکمیل کنید.">people.sh</mark>
├── <mark class="green" title="مدیریت ماتریس دسترسی و مجوزهای ورود؛ این فایل را تکمیل کنید.">perms.sh</mark>
├── <mark class="green" title="صدور و لغو badgeها؛ این فایل را تکمیل کنید.">badges.sh</mark>
├── <mark class="green" title="موتور بررسی دسترسی و مدیریت occupancy لحظهای؛ این فایل را تکمیل کنید.">access.sh</mark>
├── <mark class="green" title="ورود و خروجی گرفتن دادهها در قالب JSON و CSV؛ این فایل را تکمیل کنید.">io.sh</mark>
├── <mark class="green" title="اعتبارسنجی، تولید integrity hash، پشتیبانگیری و دریافت آمار؛ این فایل را تکمیل کنید.">integrity.sh</mark>
├── <mark class="green" title="پیادهسازی TCP daemon فقط برای خواندن اطلاعات؛ این فایل را تکمیل کنید.">serve.sh</mark>
├── <mark class="green" title="Helper پایتون برای پردازش JSON؛ این فایل را تکمیل کنید.">jsonio.py</mark>
└── shims/
└── <mark class="green" title="جایگزین Python برای sqlite3 CLI؛ این فایل را تکمیل کنید.">sqlite3</mark>
```
</details>
# **جزئیات**
ابزار همیشه به این شکل اجرا خواهد شد:
```bash terminal terminal
./solution.sh <command> [args...]
```
> فایل `solution.sh` باید ماژولهای `lib/` را با `source` بارگذاری کند. برای پیدا کردن `lib/` مسیر **خود اسکریپت** را حساب کنید، نه پوشهٔ کاری، وگرنه اجرای ابزار از یک پوشهٔ دیگر دچار مشکل خواهد شد. مسیر پوشهٔ داده از متغیر محیطی `ACCRED_DATA_DIR` خوانده میشود و اگر ست نشده بود پیشفرضش `data` در پوشهٔ کاری است. سیستم داوری همیشه این متغیر را روی یک پوشهٔ موقت ست میکند، پس حتماً از آن بخوانید و مسیر را **هاردکد نکنید.**
+ **قفل:** همهٔ عملیات نوشتن باید روی **یک فایل قفل مشترک** داخل همان پوشهٔ داده انجام شوند، مثلاً `$ACCRED_DATA_DIR/.accred.lock`. همهٔ پردازهها باید روی یک فایل قفل مشترک هماهنگ شوند؛ اگر هرکدام قفل جدا بگیرند، هماهنگی برقرار نمیشود.
+ **اولین اجرا:** اگر پوشهٔ داده یا فایل `accred.db` وجود نداشته باشد، ابزار باید خودش آنها را بسازد و اسکیما را اعمال کند. اولین اجرای هر دستور، حتی `list-zones`، باید در صورت نبودن `data/` و `accred.db` آنها را بسازد و اسکیما را اعمال کند.
|  |
| :-: |
| نقطهٔ ورود، ماژولهای دامنه و لایهٔ دادهٔ *SQLite* که همهٔ دسترسیها را با `flock` سریالی میکند. |
## **محیط اجرای سیستم داوری**
**این مهمترین محدودیت این سؤال است و اگر به آن دقت نکنید، هیچکدام از دستورها کار نخواهند کرد.**
محیط داوری *Linux* است و علاوه بر ابزارهای معمول *GNU coreutils* مثل `sort` و `uniq` و `cut` و `paste`، اینها هم تضمینشده در دسترساند: `bash`، `python3`، `flock`، `sha256sum` و `awk`.
ولی ابزارهای خط فرمانِ `sqlite3`، `jq` و `nc` روی داور **نصب نیستند**. پایتون ۳ ماژولهای `sqlite3` و `json` و `socket` را بهصورت درونساخت دارد، پس هر سه کار با پایتون شدنی است.
+ **پس این سؤال دقیقاً چقدر بش است؟** بدنه اسکلت ابزار، تجزیهٔ آرگومانها، درخت تصمیم `check`، قفلگذاری با `flock` و چاپ خروجیها همه بشاند و بخش عمدهٔ کد را میسازند. ولی چندجا ناگزیر باید چند خطی پایتون بنویسید: پل زدن به *SQLite،* ساخت و خواندن *JSON* و گوش دادن روی سوکت *TCP*. این چالش عمداً طراحی شده و همان مهارتی است که سنجیده میشود: وقتی ابزار دلخواهتان روی سروری نیست و نمیتواند هم باشد، با چیزی که هست سر میکنید. پس اگر انتظار یک چالش صددرصد بش داشتید، بدانید که نوشتن چند قطعهٔ پایتونی هم لازم خواهد شد...
+ **توجه:** برای کار با *SQLite* یک اسکریپت پایتونی جایگزینِ `sqlite3` در `lib/shims/` بسازید و ابتدای `PATH` قرارش دهید تا بقیهٔ کدتان طوری بنویسد که انگار `sqlite3` واقعی وجود دارد.
+ **توجه:** برای ساخت و خواندن *JSON* از ماژول `json` پایتون استفاده کنید، نه `jq`.
+ **توجه:** برای گوش دادن روی پورت *TCP* از ماژول `socket` پایتون استفاده کنید، نه `nc`.
<details class="pink">
<summary>**راهنمایی: ساختن جایگزین `sqlite3` با پایتون**</summary>
لازم نیست کل رفتار `sqlite3` را بنویسید. فقط همین زیرمجموعهای که کد خودتان استفاده میکند کافی است: مسیر دیتابیس را از آرگومان اول بگیرد، کوئری را از آرگومان دوم یا از ورودی استاندارد بخواند و نتیجهٔ هر سطر را با جداکنندهٔ `|` چاپ کند.
```python lib/shims/sqlite3 python
#!/usr/bin/env python3
import sqlite3, sys
db = sys.argv[1]
script = sys.argv[2] if len(sys.argv) > 2 else sys.stdin.read()
con = sqlite3.connect(db, timeout=30)
con.execute("PRAGMA foreign_keys = ON")
cur, buf = con.cursor(), ""
for chunk in script.split(";"):
buf += chunk + ";"
if not <mark class="yellow" title="تا وقتی دستور کامل نشده، تکهها را روی هم جمع میکند">sqlite3.complete_statement(buf)</mark>:
continue
if buf.strip(" \t\r\n;"):
cur.execute(buf)
for row in cur.fetchall():
print("|".join("" if v is None else str(v) for v in row))
buf = ""
con.commit()
con.close()
```
> این اسکریپت مسیر دیتابیس را از آرگومان اول میگیرد و کوئری را یا از آرگومان دوم یا از ورودی استاندارد میخواند، بعد آن را به دستورهای جداگانه میشکند و هر کدام را جدا اجرا میکند. خروجی هر سطر را با جداکنندهٔ `|` چاپ میکند، دقیقاً مثل حالت پیشفرض `sqlite3`. بعد از ساختن فایل، با `chmod +x` اجراییاش کنید و در `solution.sh` مسیر `lib/shims` را ابتدای `PATH` بگذارید.
+ **چرا با `complete_statement` و نه با `executescript`؟** چون `executescript` در پایتون هیچ سطری برنمیگرداند. اگر با آن بنویسید، دستوری مثل `UPDATE ...; SELECT changes();` بدون هیچ پیام خطایی خروجی خالی میدهد و پیدا کردن این باگ ساعتها وقت میگیرد. تابع `complete_statement` هم هوشمندتر از `split` ساده است و نقطهویرگولِ داخل رشتهها را اشتباهاً مرز دستور حساب نمیکند. نسخهٔ بالا حداقلی است و اگر کد شما به قابلیت دیگری از `sqlite3` نیاز داشت، خودتان میتوانید اضافهاش کنید.
</details>
## **اسکیمای دیتابیس**
سیستم داوری برای ساختن سناریوهای خراب، **مستقیم به دیتابیس شما وصل میشود و در جدولها `INSERT` و `UPDATE` میزند**. به همین دلیل نام فایل دیتابیس و نام این سه جدول و ستونهایشان اجباری است:
| مورد | مقدار اجباری |
| --: | --: |
| مسیر فایل دیتابیس | `$ACCRED_DATA_DIR/accred.db` |
| جدول کارتها | `badges` با ستونهای `serial`، `person`، `status` |
| جدول ماتریس دسترسی | `permissions` با ستونهای `role`، `zone` |
| جدول حضور | `occupancy` با ستونهای `zone`، `inside` |
بقیهٔ جدولها (افراد، موقعیتها، لاگ ورود، لاگ تغییرات) را با هر نام و ستونی که خواستید بسازید. لایهٔ ذخیرهسازی را عمداً کمقید نگه دارید تا دادهای که از بیرون وارد یا دستی ویرایش شده بهجای رد شدن، بعداً با دستور `validate` پیدا شود.
|  |
| :-: |
| جدولهای سامانه و پیوندهای منطقی بین افراد، کارتها، موقعیتها، ماتریس دسترسی و حضور. |
## **کدهای خروج**
همهٔ دستورها از یک قاعدهٔ مشترک برای کد خروج پیروی میکنند:
| **کد خروج** | **معنی** |
| :-: | --: |
| `0` | موفقیت |
| `1` | خطای استفاده یا مقدار **نامعتبر** (دستور ناشناخته، بدون آرگومان، ظرفیت منفی، نام تکراری) |
| `2` | ارجاع به چیزی که وجود **ندارد** (فرد، موقعیت یا فایل ناشناخته) |
| `3` | شکست یکپارچگی، یعنی وقتی `validate` مشکلی پیدا کند |
- **توجه:** پیامهای خطا روی **خروجی خطا** *(stderr)* چاپ میشوند و نباید در خروجی استاندارد بیایند.
## **دستورهای ساختاری**
| **دستور** | **کار** | **رفتار مورد انتظار** |
| --: | --: | --: |
| `help` | چاپ راهنمای ابزار | کد خروج `0` و دستکم یک خط که واژهٔ `accred` در آن باشد |
| هر دستور ناشناخته | مثلاً `no-such-command` | کد خروج `1` |
| اجرا بدون هیچ آرگومان | `./solution.sh` | کد خروج `1` |
روی یک دیتابیس خالی، `list-zones` باید با کد خروج `0` و **بدون هیچ خط خروجی** تمام شود.
## **افراد، موقعیتها و ماتریس دسترسی**
| دستور | کار | خروجی |
| --: | --: | --: |
| `add-person <name> <role> [org]` | ثبت یک فرد با نقش و سازمان اختیاری | `PERSON <name> <role> <org>` |
| `list-people [role]` | فهرست افراد **به ترتیب ثبت**، با فیلتر اختیاری نقش | هر خط: `<name> <role> <org>` |
| `add-zone <name> <capacity>` | تعریف یک موقعیت با ظرفیت | `ZONE <name> <capacity>` |
| `list-zones` | فهرست موقعیتها **مرتب شده بر اساس نام** | هر خط: `<name> <capacity> <inside>` |
| `grant <role> <zone>` | مجاز کردن یک نقش برای یک موقعیت | `GRANT <role> <zone>` |
| `revoke-perm <role> <zone>` | برداشتن مجوز یک نقش از یک موقعیت | `REVOKE-PERM <role> <zone>` |
| `permissions [role]` | ماتریس دسترسی، **مرتب شده بر اساس نقش و بعد موقعیت** | هر خط: `<role> <zone>` |
| `who-can-enter <zone>` | افرادِ دارای کارت فعال و نقش مجاز برای یک موقعیت | هر خط: `<name> <role>` مرتب شده بر اساس نام |
قواعدی که باید رعایت شوند:
- اگر `org` داده نشود، در خروجی بهجایش کاراکتر `-` میآید. مثلاً `add-person Vahid VIP` خروجی `PERSON Vahid VIP -` میدهد.
- ثبت فردی با **نام تکراری** رد میشود و کد خروج `1` میدهد.
- ظرفیت موقعیت باید یک **عدد صحیح نامنفی** باشد. مقدارهایی مثل `-1` یا `abc` کد خروج `1` میدهند.
- دستور `grant` روی موقعیتی که وجود ندارد کد خروج `2` میدهد. `leave` هم روی موقعیت ناشناخته همین `2` را میدهد. قاعدهٔ کلی این است که **هر ارجاع به چیزی که وجود ندارد** کد `2` میگیرد، چه در دستور خواندنی باشد چه نوشتنی.
- دستور `who-can-enter` روی موقعیت ناشناخته کد خروج `2` میدهد و کسانی را که کارتشان باطل شده در فهرست نمیآورد.
## **کارتها و منطق تصمیم ورود**
نام هر فرد در کل سامانه **یکتاست**. اگر `add-person` با نامی صدا زده شود که از قبل ثبت شده، باید آن را **رد کند** و کد خروج `1` بدهد، نه اینکه نقش یا سازمانش را بهروز کند. سریال هر کارت هم به همین شکل یکتاست.
هر فرد میتواند یک یا چند کارت با سریال یکتا داشته باشد. کارت تا وقتی **باطل** نشده وضعیت `ACTIVE` دارد. مهمترین دستور این سامانه `check` است که تصمیم ورود را میگیرد و اگر پذیرفت، تعداد افراد داخل آن موقعیت را یکی زیاد میکند.
| **دستور** | **کار** | **خروجی** |
| --: | --: | --: |
| `issue <person> <serial>` | صدور کارت برای یک فرد | `BADGE <serial> <person> ACTIVE` |
| `revoke <serial>` | باطل کردن یک کارت | `BADGE <serial> <person> REVOKED` |
| `list-badges [person]` | فهرست کارتها **مرتب شده بر اساس سریال** | هر خط: `<serial> <person> <status>` |
| `check <serial> <zone>` | تلاش برای ورود | `<GRANTED>` یا `<DENIED>` و بعد `<serial> <zone> <reason>` |
| `leave <serial> <zone>` | خروج از یک موقعیت | `LEFT <serial> <zone>` |
| `occupancy [zone]` | تعداد افراد داخل | هر خط: `<zone> <inside>/<capacity>` |
| `access-report [zone]` | شمار پذیرش و رد | هر خط: `<zone> GRANTED=<g> DENIED=<d>` |
دستور `check` شرطها را **به همین ترتیب** بررسی میکند و `reason` یکی از این پنج مقدار میشود:
| ترتیب بررسی | شرط | مقدار `reason` | نتیجه |
| :-: | --: | --: | --: |
| ۱ | کارت اصلاً وجود ندارد | `NO_BADGE` | `DENIED` |
| ۲ | کارت باطل شده است | `REVOKED` | `DENIED` |
| ۳ | نقشِ دارندهٔ کارت برای این موقعیت مجاز نیست | `NOT_PERMITTED` | `DENIED` |
| ۴ | موقعیت پر است | `ZONE_FULL` | `DENIED` |
| ۵ | هیچکدام از بالا | `OK` | `GRANTED` |
- **دستور** `check` همیشه با کد خروج `0` تمام میشود، چون **رد شدن یک تصمیم عادی است، نه خطا**. فقط موقعیت ناشناخته کد خروج `2` میدهد.
- هر `check` که به `DENIED` برسد نباید تعداد افراد داخل موقعیت را عوض کند.
- موقعیتی با ظرفیت `0` همه را رد میکند و دلیلش `ZONE_FULL` است.
- **دستور** `leave` فقط تعداد افراد داخل موقعیت را یکی کم میکند و **هرگز زیر صفر نمیرود**. این دستور بررسی نمیکند که آن کارت واقعاً داخل موقعیت بوده یا نه؛ فقط وجود موقعیت را چک میکند و روی موقعیت ناشناخته کد خروج `2` میدهد.
- به همین ترتیب، اگر یک کارت دو بار پشت سر هم `check` شود و هر دو بار پذیرفته شود، تعداد افراد داخل **دو واحد** بالا میرود. سامانه حضور تکتک کارتها را ردیابی نمیکند و فقط یک شمارندهٔ عددی برای هر موقعیت دارد.
- **دستور** `occupancy` بدون آرگومان همهٔ موقعیتها را مرتب شده بر اساس نام چاپ میکند و روی موقعیت ناشناخته کد خروج `2` میدهد.
- موقعیت تازهساخته باید **بلافاصله** در `occupancy` با مقدار `0` دیده شود، حتی وقتی هنوز هیچکس داخلش نرفته. سادهترین راه این است که همان `add-zone` علاوه بر ردیف موقعیت، یک ردیف حضور با مقدار صفر هم بسازد؛ وگرنه باید موقع خواندن، نبودِ ردیف را صفر حساب کنید.
- دستور `access-report` وقتی هیچ `check`ای انجام نشده، با کد خروج `0` و بدون هیچ خط خروجی تمام میشود.
|  |
| :-: |
| منطق `check`: بررسیِ وجود کارت، وضعیت کارت، مجوز نقش و ظرفیت موقعیت و در پایان یکی از پنج نتیجه. |
<details class="red">
<summary>**هشدار: پذیرش همزمان و رقابت روی ظرفیت**</summary>
سیستم داوری چند دستور `check` را **همزمان و روی یک موقعیت** اجرا میکند. اگر «خواندن تعداد فعلی»، «مقایسه با ظرفیت» و «اضافه کردن یک واحد» را سه دستور جدا انجام دهید، دو پروسهٔ همزمان میتوانند هر دو ببینند که موقعیت جا دارد و هر دو وارد شوند و تعداد افراد داخل از ظرفیت رد شود.
برای درست کار کردن باید این سه گام یک **عملیات اتمیک** باشند. سادهترین راه یک `UPDATE` شرطی است که فقط وقتی تعداد فعلی کمتر از ظرفیت باشد یکی اضافه میکند و بعد با `changes()` میفهمید ردیفی تغییر کرد یا نه.
لایهٔ دادهٔ شما باید هر دسترسی را با `flock` سریالی کند، اما اتمیک بودنِ منطقِ پذیرش بر عهدهٔ خودتان است. دو تست این بخش، پانزده و دوازده `check` همزمان میفرستند و انتظار دارند نه هیچ ورودی گم شود و نه تعداد افراد از ظرفیت رد شود.
</details>
|  |
| :-: |
| چرا خواندن و نوشتنِ جدا خطرناک است: دو گیت همزمان هر دو ظرفیت خالی میبینند و تعداد افراد از ظرفیت رد میشود؛ راهحل، یک پذیرشِ اتمیک است. |
## **داده، یکپارچگی و سرور**
| **دستور** | **عملیات** |
| --: | --: |
| `export-json` و `dump-json` | چاپ کل دیتابیس بهصورت *JSON* مرتب و قطعی. خروجی این دو باید **کاملاً یکسان** باشد |
| `export-csv` | چاپ افراد بهصورت *CSV* با سطر هدر |
| `import-json <file>` | بارگذاری افراد، موقعیتها، مجوزها و کارتها از یک فایل *JSON* |
| `validate` | بررسی یکپارچگی و چاپ خطاهای مرتب شده |
| `integrity-check` | هش *SHA-256* مستقل از ترتیب از همان چهار موجودیتی که `export-json` میدهد |
| `backup <file>` و `restore <file>` | پشتیبانگیری و بازگرداندن کامل دیتابیس |
| `audit-log [n]` و `statistics` | گزارش تغییرات ثبتشده و آمار کلی |
| `serve <port> [--once]` | سرور *TCP* فقطخواندنی |
قالب دقیق خروجی هرکدام:
**`export-json`:** یک سند *JSON* با دقیقاً چهار کلید سطح بالا `badges`، `people`، `permissions` و `zones`. آرایهٔ `people` باید **مرتب شده بر اساس نام** باشد و هر عضوش کلید `name` داشته باشد.
**`export-csv`:** خط اول دقیقاً `name,role,org` و بعد یک خط برای هر فرد، مثلاً `Sara,PRESS,IRIB`.
**`import-json <file>`:** باید طوری داده را بازسازی کند که اگر خروجی `export-json` یک دیتابیس را در دیتابیس خالیِ دیگری وارد کنید، `integrity-check` هر دو **عدد یکسانی** بدهد.
+ **این شرط دامنهٔ `integrity-check` را تعیین میکند.** هش باید فقط روی همان چهار موجودیتِ `badges`، `people`، `permissions` و `zones` حساب شود؛ یعنی همانهایی که از `export-json` بیرون میآیند و `import-json` بازسازیشان میکند. اگر لاگ تغییرات یا شمارش حضور یا آمار پذیرش را هم داخل هش بیاورید، دیتابیس تازهواردشده هرگز به همان عدد نمیرسد، چون آن دادهها در سند *JSON* نیستند. مثل سؤالهای دیگر، ترتیبناپذیری را با `LC_ALL=C sort` روی سطرها قبل از `sha256sum` بگیرید.
**`validate`:** روی دیتابیس سالم فقط واژهٔ `OK` را چاپ میکند و کد خروج `0` میدهد. اگر مشکلی پیدا کند، کد خروج `3` میدهد و برای هر مشکل یک خط چاپ میکند:
```text output terminal
BADGE <serial> UNKNOWN_PERSON
PERM <role> <zone> UNKNOWN_ZONE
ZONE <zone> OVER_CAPACITY <inside>/<capacity>
```
> خط اول برای کارتی است که به فردی اشاره میکند که وجود ندارد، خط دوم برای مجوزی که به موقعیت ناشناخته اشاره میکند و خط سوم برای موقعیتی که تعداد افراد داخلش از ظرفیتش رد کرده است. سیستم داوری این حالتها را با نوشتن مستقیم در دیتابیس شما میسازد، برای همین بود که نام جدولها و ستونها در بخش اسکیما اجباری شد.
**`integrity-check`:** دقیقاً یک خط به شکل `INTEGRITY <hex>` که در آن `<hex>` یک هش *SHA-256* شصتوچهار کاراکتری با حروف کوچک است.
+ **قالب سریالایز دلخواه خودتان است** و با هیچ مقدار ثابتی مقایسه نمیشود. فقط این سه رفتار سنجیده میشود: اجرای دوباره روی دادهٔ عوضنشده همان هش را بدهد، اضافه شدن حتی یک فرد هش را عوض کند و وارد کردن خروجی `export-json` در یک دیتابیس خالی همان هش را بازتولید کند. برای همین کافی است سریالایز شما مرتب و قطعی باشد و به شناسههای داخلی *SQLite* مثل `rowid` وابسته نباشد.
**`backup <file>` و `restore <file>`:** بعد از `backup`، تغییر دادن داده و بعد `restore`، مقدار `integrity-check` باید به همان عدد قبل از `backup` برگردد. دستور `restore` روی فایلی که وجود ندارد کد خروج `2` میدهد.
**`audit-log [n]`:** هر تغییر ثبتشده در یک خط، که خط با **نام همان دستور** شروع میشود، مثلاً `add-person ...` یا `add-zone ...`.
**`statistics`:** باید دستکم این خطها را داشته باشد:
```text output terminal
people=3
zones=2
badges=3 active=3
access granted=1 denied=1
```
> هر خط یک شمارش از وضعیت فعلی است: تعداد افراد، تعداد موقعیتها، تعداد کل کارتها بههمراه تعداد کارتهای فعال و تعداد تصمیمهای پذیرش و رد. سیستم داوری وجود این خطها را در خروجی میسنجد، پس دقیقاً همین قالب را رعایت کنید. میتوانید خطهای بیشتری هم چاپ کنید.
**`serve <port> [--once]` و `__handle`:** سرور فقط به زیرمجموعهای از دستورهای فقطخواندنی مانند `ping`، `occupancy`، `permissions`، `who-can-enter` و `list-*` پاسخ میدهد و هر دستور تغییردهنده را با یک خط که با `ERROR` شروع میشود رد میکند. دستور `ping` باید `PONG` برگرداند.
+ **توجه:** علاوه بر `serve`، ابزار شما باید یک زیردستور داخلی به نام `__handle` هم داشته باشد. این زیردستور **یک خط درخواست را از ورودی استاندارد میخواند**، همان منطق فقطخواندنی را اجرا میکند و پاسخ را در خروجی استاندارد چاپ میکند. سیستم داوری هم `__handle` را مستقیم صدا میزند و هم با `serve <port> --once` یک اتصال *TCP* واقعی برقرار میکند و انتظار پاسخ `PONG` دارد.
|  |
| :-: |
| ماتریس دسترسیِ نقشها به موقعیتها و اینکه چگونه هر تلاش ورود در برابر آن سنجیده میشود. |
# **نمونه**
فرض کنید سامانه را اینطور راهاندازی کردهایم و بعد چند تلاش ورود را میسنجیم:
```text session terminal
$ ./solution.sh add-person Sara PRESS IRIB
PERSON Sara PRESS IRIB
$ ./solution.sh add-zone PRESSBOX 3
ZONE PRESSBOX 3
$ ./solution.sh grant PRESS PRESSBOX
GRANT PRESS PRESSBOX
$ ./solution.sh issue Sara B-1
BADGE B-1 Sara ACTIVE
$ ./solution.sh check B-1 PRESSBOX
GRANTED B-1 PRESSBOX OK
$ ./solution.sh occupancy PRESSBOX
PRESSBOX 1/3
```
> اول فرد `Sara` با نقش `PRESS` ثبت و موقعیت `PRESSBOX` با ظرفیت `3` تعریف میشود، بعد نقش `PRESS` برای این موقعیت مجاز و یک کارت `B-1` برای `Sara` صادر میشود. وقتی کارت `B-1` برای ورود به `PRESSBOX` سنجیده میشود، چون کارت فعال است و نقشش مجاز است و موقعیت جا دارد، نتیجه `GRANTED B-1 PRESSBOX OK` میشود و تعداد افراد داخل به `1/3` میرسد. اگر همین کارت را برای موقعیتی میسنجیدیم که نقش `PRESS` در آن مجاز نبود، نتیجه `DENIED B-1 <zone> NOT_PERMITTED` بود و عدد حضور تغییری نمیکرد.
حالا فرض کنید کارت را باطل کنیم و دوباره بسنجیم:
```text session terminal
$ ./solution.sh revoke B-1
BADGE B-1 Sara REVOKED
$ ./solution.sh check B-1 PRESSBOX
DENIED B-1 PRESSBOX REVOKED
$ ./solution.sh integrity-check
INTEGRITY 3f2a9c1b8e47d05a6c2f1b93ae70d4c8f5619b2ad3e08c74195fb6237ce0a8d1
```
> بعد از باطل شدن، وضعیت کارت `REVOKED` میشود و تلاش بعدی ورود با دلیل `REVOKED` رد میشود، بدون اینکه کد خروج خطا بدهد. دستور `integrity-check` هم یک هش *SHA-256* از وضعیت میسازد که تا وقتی داده عوض نشود ثابت میماند و اگر حتی یک فرد یا کارت اضافه شود عوض میشود. عددی که اینجا میبینید فقط یک نمونهٔ شصتوچهار کاراکتری است و قرار نیست با خروجی شما یکی باشد؛ آنچه سنجیده میشود قالب خط و **پایداری** همان عدد است، نه مقدارش.
|  |
| :-: |
| مسیر داده: ثبت فرد و موقعیت، صدور کارت، سنجش ورود در برابر ماتریس دسترسی و ظرفیت و بهروزرسانی تعداد افراد داخل. |
|  |
| :-: |
| همهٔ پردازهها روی یک قفل مشترک هماهنگ میشوند و ظرفیت هرگز از حد رد نمیشود. |
+ **دو بخش از این جدول بهصورت «همه یا هیچ» نمره میگیرند** و در آنها امتیاز جزئی وجود ندارد: پذیرش همزمان با `flock` و اجرای سناریوی کامل. یعنی اگر از دو تست همزمانی یکی رد شود، هر شش امتیاز آن بخش میرود. بقیهٔ بخشها معمولیاند و بهازای هر تست موفق امتیاز میدهند.
## **نقشهٔ امتیازها**
از این جدول برای انتخاب مسیر خودتان استفاده کنید. هر ردیف مستقل نمره میگیرد:
| **بخش** | **سابتسک** | **درصد** |
| --: | --: | :-: |
| ساختار و راهنما | `help`، کد خروج دستور ناشناخته، اجرای تمیز روی دیتابیس خالی | ۲ |
| افراد | `add-person`، `list-people` | ۵ |
| موقعیتها | `add-zone`، `list-zones` | ۴ |
| ماتریس دسترسی | `grant`، `revoke-perm`، `permissions` | ۶ |
| کارتها | `issue`، `revoke`، `list-badges` | ۶ |
| پذیرش موفق | `check` وقتی مجاز است | ۵ |
| رد کارت ناشناخته یا باطل | `check` با `NO_BADGE` و `REVOKED` | ۶ |
| رد نقش غیرمجاز | `check` با `NOT_PERMITTED` | ۵ |
| رد بهخاطر پر بودن موقعیت | `check` با `ZONE_FULL` | ۶ |
| خروج از موقعیت | `leave` | ۴ |
| گزارش حضور | `occupancy` | ۴ |
| فهرست مجازها | `who-can-enter` | ۴ |
| گزارش پذیرش و رد | `access-report` | ۴ |
| پذیرش همزمان | درست ماندن `check` زیر اجرای موازی | ۶ |
| ورودی و خروجی داده | `export-json`، `dump-json`، `export-csv`، `import-json` | ۵ |
| بررسی یکپارچگی | `validate` | ۵ |
| هش یکپارچگی | `integrity-check` | ۴ |
| پشتیبان و بازیابی | `backup`، `restore` | ۴ |
| لاگ و آمار | `audit-log`، `statistics` | ۵ |
| تطبیق با مدل مرجع | یک سناریوی کامل که با مدل مستقل سنجیده میشود | ۵ |
| سرور فقطخواندنی | `serve` و `__handle` | ۵ |
> اگر وقتتان محدود است، از بالای جدول شروع کنید. ردیفهای «افراد» تا «کارتها» با هم ۲۱ درصد دارند و هیچکدام به *SQLite* پیچیده یا پایتون نیاز ندارند. بعد سراغ چهار ردیف `check` بروید که روی هم ۲۲ درصد امتیازند و هستهٔ فنی سؤالند. ردیفهای داده و سرور را برای آخر بگذارید: امتیازشان بخش خوبی از سوال است ولی هرکدام یک قطعهٔ پایتونی جدا لازم دارند.
# **آنچه باید آپلود کنید**
کل درخت پروژه را بسازید و آپلود کنید. لازم نیست همهٔ ماژولها را پیاده کنید؛ هر بخش امتیاز خودش را جداگانه میگیرد.
فایلهایی که سیستم داوری میپذیرد:
- `solution.sh`
- `lib/*.sh`
- `lib/*.py`
- `lib/*.sql`
- `lib/shims/*`
+ **توجه:** مسیر پوشهٔ داده را از **متغیر محیطی** `ACCRED_DATA_DIR` بخوانید و فایل دیتابیس را **دقیقاً** `accred.db` نام بگذارید.
+ **توجه:** چون `sqlite3` و `jq` و `nc` روی داور نصب نیستند، کار با *SQLite* و *JSON* و *TCP* را با `python3` انجام دهید.
+ **توجه:** برای هش `integrity-check` از `printf '%s'` استفاده کنید تا خط جدید اضافهای وارد ورودی هش نشود و سریالایز را مرتب و قطعی بسازید.
+ **توجه:** منطق پذیرش در `check` باید زیر اجرای همزمان چند گیت درست بماند و تعداد افراد داخل هرگز از ظرفیت رد نشود.
+ **توجه:** اگر فایلهای `lib/shims/*` را اجرایی نمیفرستید، در `solution.sh` خودتان با `chmod +x` قابل اجرایشان کنید.
ارسال پاسخ برای این سؤال
در حال حاضر شما دسترسی ندارید.