Skip to content

Instantly share code, notes, and snippets.

@KorigamiK
Created March 20, 2026 01:44
Show Gist options
  • Select an option

  • Save KorigamiK/5fa609a605e53531242eba7961da8d93 to your computer and use it in GitHub Desktop.

Select an option

Save KorigamiK/5fa609a605e53531242eba7961da8d93 to your computer and use it in GitHub Desktop.
Extract the files from a firefox profile
#!/usr/bin/env python3
"""Extract tab details from a Firefox/Zen profile or sessionstore file."""
from __future__ import annotations
import argparse
import json
import struct
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
MAGIC = b"mozLz40\0"
SESSION_CHOICES = {
"recovery": "sessionstore-backups/recovery.jsonlz4",
"sessionstore": "sessionstore.jsonlz4",
"recovery-bak": "sessionstore-backups/recovery.baklz4",
"previous": "sessionstore-backups/previous.jsonlz4",
}
def decompress_mozlz4(payload: bytes) -> bytes:
if not payload.startswith(MAGIC):
raise ValueError("Not a Mozilla jsonlz4 file")
expected_size = struct.unpack_from("<I", payload, len(MAGIC))[0]
data = payload[len(MAGIC) + 4 :]
out = bytearray()
i = 0
n = len(data)
while i < n:
token = data[i]
i += 1
literal_length = token >> 4
if literal_length == 15:
while True:
extra = data[i]
i += 1
literal_length += extra
if extra != 255:
break
out.extend(data[i : i + literal_length])
i += literal_length
if i >= n:
break
offset = data[i] | (data[i + 1] << 8)
i += 2
if offset <= 0 or offset > len(out):
raise ValueError(f"Invalid LZ4 offset: {offset}")
match_length = (token & 0x0F) + 4
if (token & 0x0F) == 15:
while True:
extra = data[i]
i += 1
match_length += extra
if extra != 255:
break
start = len(out) - offset
while match_length > 0:
chunk = out[start : start + min(match_length, offset)]
if not chunk:
raise ValueError("Empty overlap chunk while decompressing")
out.extend(chunk)
match_length -= len(chunk)
if len(out) != expected_size:
raise ValueError(
f"Decompressed size {len(out)} does not match expected {expected_size}"
)
return bytes(out)
def load_session(path: Path) -> dict[str, Any]:
raw = path.read_bytes()
if path.suffix == ".json":
return json.loads(raw.decode("utf-8"))
return json.loads(decompress_mozlz4(raw).decode("utf-8"))
def resolve_session_file(path: Path, session_name: str) -> Path:
if path.is_file():
return path
if not path.is_dir():
raise FileNotFoundError(f"No such file or directory: {path}")
if session_name != "auto":
candidate = path / SESSION_CHOICES[session_name]
if candidate.exists():
return candidate
raise FileNotFoundError(f"Could not find {candidate}")
candidates = [path / relative for relative in SESSION_CHOICES.values()]
candidates.extend(sorted(path.glob("sessionstore-backups/upgrade.jsonlz4-*")))
existing = [candidate for candidate in candidates if candidate.exists()]
if not existing:
raise FileNotFoundError(
f"No Firefox session files found under profile directory: {path}"
)
return max(existing, key=lambda candidate: candidate.stat().st_mtime)
def current_entry(tab: dict[str, Any]) -> dict[str, Any]:
entries = tab.get("entries") or []
if not entries:
return {}
index = (tab.get("index") or 1) - 1
if 0 <= index < len(entries):
return entries[index]
return entries[-1]
def iso_time(unix_ms: Any) -> str | None:
if not unix_ms:
return None
return datetime.fromtimestamp(unix_ms / 1000).astimezone().isoformat()
def extract_tab(
tab: dict[str, Any], tab_index: int, selected_index: int, include_history: bool
) -> dict[str, Any]:
entry = current_entry(tab)
ext_data = tab.get("extData") or {}
storage = tab.get("storage") or {}
item = {
"tab_index": tab_index,
"active": tab_index == selected_index,
"title": entry.get("title"),
"url": entry.get("url"),
"original_uri": entry.get("originalURI"),
"pinned": bool(tab.get("pinned")),
"hidden": bool(tab.get("hidden")),
"user_context_id": tab.get("userContextId"),
"last_accessed_unix_ms": tab.get("lastAccessed"),
"last_accessed_local": iso_time(tab.get("lastAccessed")),
"history_length": len(tab.get("entries") or []),
"workspace_id": tab.get("zenWorkspace"),
"extension_group_id": ext_data.get(
"extension:{dcdaadfa-21f1-4853-9b34-aad681fff6f3}:groupId"
),
"has_formdata": "formdata" in tab,
"has_storage": "storage" in tab,
"storage_origins": sorted(storage.keys()),
"attributes": tab.get("attributes") or {},
"ext_data": ext_data,
}
if include_history:
item["history"] = [
{
"index": idx,
"title": entry_item.get("title"),
"url": entry_item.get("url"),
"original_uri": entry_item.get("originalURI"),
}
for idx, entry_item in enumerate(tab.get("entries") or [], start=1)
]
return item
def build_report(
source_path: Path, session_file: Path, include_history: bool
) -> dict[str, Any]:
session = load_session(session_file)
windows = session.get("windows") or []
report: dict[str, Any] = {
"source_path": str(source_path),
"session_file": str(session_file),
"session_file_mtime_local": datetime.fromtimestamp(
session_file.stat().st_mtime
).astimezone().isoformat(),
"window_count": len(windows),
"selected_window": session.get("selectedWindow"),
"session": session.get("session") or {},
"tabs_total": 0,
"windows": [],
}
for window_index, window in enumerate(windows, start=1):
tabs = window.get("tabs") or []
selected_index = window.get("selected") or 1
window_item = {
"window_index": window_index,
"selected_tab_index": selected_index,
"tab_count": len(tabs),
"pinned_count": sum(1 for tab in tabs if tab.get("pinned")),
"hidden_count": sum(1 for tab in tabs if tab.get("hidden")),
"tabs": [
extract_tab(tab, idx, selected_index, include_history)
for idx, tab in enumerate(tabs, start=1)
],
}
report["tabs_total"] += len(tabs)
report["windows"].append(window_item)
return report
def print_summary(report: dict[str, Any]) -> None:
print(f"Source: {report['source_path']}")
print(f"Session file: {report['session_file']}")
print(
f"Windows: {report['window_count']} | Tabs: {report['tabs_total']} | "
f"Saved: {report['session_file_mtime_local']}"
)
for window in report["windows"]:
print(
f"\nWindow {window['window_index']}: "
f"{window['tab_count']} tabs, "
f"{window['pinned_count']} pinned, "
f"{window['hidden_count']} hidden"
)
for tab in window["tabs"]:
flags = []
if tab["active"]:
flags.append("ACTIVE")
if tab["pinned"]:
flags.append("PINNED")
if tab["hidden"]:
flags.append("HIDDEN")
suffix = f" [{' | '.join(flags)}]" if flags else ""
print(f"{tab['tab_index']}. {tab['title'] or '(no title)'}{suffix}")
print(f" URL: {tab['url'] or '(no url)'}")
print(
" Last accessed: "
f"{tab['last_accessed_local'] or 'unknown'} | "
f"Workspace: {tab['workspace_id'] or '-'}"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Extract tab details from a Firefox or Zen profile directory, or from "
"a direct sessionstore*.jsonlz4 file."
)
)
parser.add_argument(
"path",
help=(
"Profile directory such as ~/.mozilla/firefox/xxxx.default-release or "
"~/.zen/xxxx, or a direct sessionstore*.jsonlz4 file."
),
)
parser.add_argument(
"--session-file",
choices=["auto", *SESSION_CHOICES],
default="auto",
help="Which session file to use when you pass a profile directory.",
)
parser.add_argument(
"--include-history",
action="store_true",
help="Include simplified navigation history for each tab.",
)
parser.add_argument(
"--summary",
action="store_true",
help="Print a readable summary instead of full JSON.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
source_path = Path(args.path).expanduser()
try:
session_file = resolve_session_file(source_path, args.session_file)
report = build_report(source_path, session_file, args.include_history)
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
if args.summary:
print_summary(report)
else:
print(json.dumps(report, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment