Skip to content

Instantly share code, notes, and snippets.

@sfoster
Forked from IntendedConsequence/read-chrome-sessions.py
Last active November 7, 2025 00:41
Show Gist options
  • Save sfoster/01951fb61d8cc6fcf9cae450882c1320 to your computer and use it in GitHub Desktop.
Save sfoster/01951fb61d8cc6fcf9cae450882c1320 to your computer and use it in GitHub Desktop.
Parse urls and titles from chrome browser sessions
import sys
import os
import csv
import json
import struct
import argparse
from io import BytesIO
from enum import Enum, EnumMeta
from urllib.parse import unquote
from pathlib import Path
"""
SNSS Session File Parser for PyBrinf
This module parses SNSS files and returns a list of current tabs.
Script made by Manuel Cabral
Modified by YourMom for proper string padding alignment and String16 support
Modified by sfoster to output a list of SessionCommand types
"""
snss_type_to_command_name = {
0: "CommandSetTabWindow",
1: "CommandSetWindowBoundsObsolete",
2: "CommandSetTabIndexInWindow",
3: "CommandTabClosed",
4: "CommandWindowClosed",
5: "CommandTabNavigationPathPrunedFromBack",
6: "CommandUpdateTabNavigation",
7: "CommandSetSelectedNavigationIndex",
8: "CommandSetSelectedTabInIndex",
9: "CommandSetWindowType",
11: "CommandTabNavigationPathPrunedFromFront",
12: "CommandSetPinnedState",
13: "CommandSetExtensionAppID",
14: "CommandSetWindowBounds3",
15: "CommandSetWindowAppName",
16: "CommandTabClosed",
17: "CommandWindowClosed",
18: "CommandSetTabUserAgentOverride",
19: "CommandSessionStorageAssociated",
20: "CommandSetActiveWindow",
21: "CommandLastActiveTime",
22: "CommandSetWindowWorkspaceObsolete",
23: "CommandSetWindowWorkspace2",
24: "CommandTabNavigationPathPruned",
25: "CommandSetTabGroup",
26: "CommandSetTabGroupMetadataObsolete",
27: "CommandSetTabGroupMetadata2",
28: "CommandSetTabGuid",
29: "CommandSetTabUserAgentOverride2",
30: "CommandSetTabData",
31: "CommandSetWindowUserTitle",
32: "CommandSetWindowVisibleOnAllWorkspaces",
33: "CommandAddTabExtraData",
34: "CommandAddWindowExtraData",
35: "CommandSetPlatformSessionId",
36: "CommandSetSplitTab",
37: "CommandSetSplitTabData",
255: "InitialStateMarkerCommandId",
}
class MetaEnum(EnumMeta):
"""This class is used for creating an enum with a custom __contains__ method"""
def __contains__(cls, item: int) -> bool:
try:
cls(item)
except ValueError:
return False
return True
class SNSSTypeCommand(Enum, metaclass=MetaEnum):
"""Enum for SNSS command types"""
CommandSetTabWindow = 0
CommandSetWindowBoundsObsolete = 1
CommandSetTabIndexInWindow = 2
CommandTabNavigationPathPrunedFromBack = 5
CommandUpdateTabNavigation = 6
CommandSetSelectedNavigationIndex = 7
CommandSetSelectedTabInIndex = 8
CommandSetWindowType = 9
CommandTabNavigationPathPrunedFromFront = 11
CommandSetPinnedState = 12
CommandSetExtensionAppID = 13
CommandSetWindowBounds3= 14
CommandSetWindowAppName = 15
CommandTabClosed = 16
CommandWindowClosed = 17
CommandSetTabUserAgentOverride = 18
CommandSessionStorageAssociated = 19
CommandSetActiveWindow = 20
CommandLastActiveTime = 21
CommandSetWindowWorkspaceObsolete = 22
CommandSetWindowWorkspace2 = 23
CommandTabNavigationPathPruned = 24
CommandSetTabGroup = 25
CommandSetTabGroupMetadataObsolete = 26
CommandSetTabGroupMetadata2 = 27
CommandSetTabGuid = 28
CommandSetTabUserAgentOverride2 = 29
CommandSetTabData = 30
CommandSetWindowUserTitle = 31
CommandSetWindowVisibleOnAllWorkspaces = 32
CommandAddTabExtraData = 33
CommandAddWindowExtraData = 34
CommandSetPlatformSessionId = 35
CommandSetSplitTab = 36
CommandSetSplitTabData = 37
InitialStateMarkerCommandId = 255
class SNSSCommand:
"""Command class core"""
def __init__(self, id: int, content: bytes):
self.id = id
self.content = content
def parse_commands(commands):
output = []
for command in commands:
if command.id in SNSSTypeCommand:
output.append(command.id)
return output
def parse_navigation_commands(commands):
output = []
for command in commands:
if command.id in SNSSTypeCommand:
if command.id == SNSSTypeCommand.CommandUpdateTabNavigation.value:
# Extracting the pickle payload
content = BytesIO(command.content)
# Get the size of the pickle
content.seek(0, os.SEEK_END)
pickle_size = content.tell()
content.seek(0, os.SEEK_SET)
# Get the size of the payload, unpacking uint32
(payload_size,) = struct.unpack("I", content.read(4))
payloard_start = pickle_size - payload_size
# Get the payload, unpacking uint32
(tab_id,) = struct.unpack("I", content.read(4))
(index,) = struct.unpack("I", content.read(4))
def read_str8():
# Get the url size, unpacking uint32
(str_length,) = struct.unpack("I", content.read(4))
padding = 4 - (str_length % 4) if str_length % 4 else 0
# If the url size is more than the difference between
# the pickle size and the current position, it's invalid
if str_length > pickle_size - content.tell():
raise Exception("Invalid string length")
return content.read(str_length + padding)[:str_length].decode(
"utf-8", "ignore"
)
def read_str16():
# Get the url size, unpacking uint32
(str_length,) = struct.unpack("I", content.read(4))
str_length = str_length * 2
padding = 4 - (str_length % 4) if str_length % 4 else 0
# if str_length % 4 != 0:
# str_length += 4 - (str_length % 4)
# If the url size is more than the difference between
# the pickle size and the current position, it's invalid
if str_length > pickle_size - content.tell():
# raise Exception("Invalid string length")
return "undefined"
return content.read(str_length + padding)[:str_length].decode(
"utf-16", "ignore"
)
# else:
url = read_str8()
title = read_str16()
output.append((tab_id, index, url, title))
return output
class InvalidSNSSFileException(Exception):
pass
def parse(path):
# Parse a SNSS Session File to get all commands
commands = []
file = open(path, "rb")
# Getting file size
file.seek(0, os.SEEK_END)
end = file.tell()
file.seek(0, os.SEEK_SET)
# Reading signature, unpacking int32
(signature,) = struct.unpack("i", file.read(4))
if signature != 0x53534E53: # SNSS
raise InvalidSNSSFileException("Invalid SNSS file")
# Reading version version, unpacking int32
(version,) = struct.unpack("i", file.read(4))
while end - file.tell() > 0:
# Read command size, unpacking uint16
(command_size,) = struct.unpack("H", file.read(2))
if command_size == 0:
raise Exception("Invalid command size, maybe corrupted file")
# Read command id, unpacking uint8
(id,) = struct.unpack("B", file.read(1))
# Read content, NOTE: command id is included in command_size
content = file.read(command_size - 1)
command = SNSSCommand(id, content)
commands.append(command)
file.close()
return commands
def main():
parser = argparse.ArgumentParser()
parser.add_argument("path", type=Path)
args = parser.parse_args()
path = args.path
if path.exists():
paths = [path] if not path.is_dir() else [x for x in path.glob("*.*") if x.is_file() and x.stat().st_size > 4]
data = {}
for path in paths:
try:
commands = parse(path)
except InvalidSNSSFileException:
print(f"File is not a valid SNSS format, skipping: {path}", file=sys.stderr)
continue
pathkey = str(path)
data[pathkey] = []
for snss_command in commands:
command_name = snss_type_to_command_name[snss_command.id] if snss_command.id in snss_type_to_command_name else "unknown type"
data[pathkey].append(f"{snss_command.id}\t{command_name}")
print(pathkey)
print("\n".join(data[pathkey]))
else:
print(f"File not found: {path}", file=sys.stderr)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment