|
-
October 03, 2024, 07:03:59 pm
- Welcome, Guest
News:Official site launch very soon, hurrah!
Show Posts
This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.
Messages - Dakusan
1
« on: Yesterday at 03:18:20 am »
My Plex server decided to go through my database and screw with all the posters and art, so I made the below script which can be used to recover them. You should only use this if doing a full revert to backup is not a viable option. Do also note that if you have refreshed the metadata on any tv series since the backup, it could cause issues with the episode posters.
#Run the following commands as root sudo su
#User variables SERVICE_NAME='plexmediaserver' BACKUP_LOCATION='/ROOT_BACKUP_LOCATION/' PLEX_USER='plex'
#Derived variables (Slashes at end of directory paths are required) SQLITE3_PATH="/usr/lib/$SERVICE_NAME/Plex SQLite" DB_PATH="/var/lib/$SERVICE_NAME/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db" IMG_PATH="/var/lib/$SERVICE_NAME/Library/Application Support/Plex Media Server/Metadata/" CACHE_PATH="/var/lib/$SERVICE_NAME/Library/Application Support/Plex Media Server/Cache/PhotoTranscoder/"
#Stop the plex service until we have updated everything echo "Stopping Plex" systemctl stop "$SERVICE_NAME"
#Set the (user_thumb_url, user_art_url, and updated_at) from the old database in the new database. #The updated_at is required to match the cached images echo "Updating database" sudo -u "$PLEX_USER" "$SQLITE3_PATH" "$DB_PATH" -cmd "ATTACH DATABASE '$BACKUP_LOCATION$DB_PATH' as OLDDB" "
UPDATE metadata_items AS NMDI SET user_thumb_url=OMDI.user_thumb_url, user_art_url=OMDI.user_art_url, updated_at=OMDI.updated_at FROM OLDDB.metadata_items AS OMDI WHERE NMDI.id=OMDI.id
" #Copy the posters and art from the old directory to the new directory echo "Copying posters" cd "$BACKUP_LOCATION$IMG_PATH" find -type d \( -name "art" -o -name "posters" \) | xargs -n100 bash -c 'rsync -a --relative "$@" "'"$IMG_PATH"'"' _
#Copy the cached images over echo "Copying cache" rsync -a "$BACKUP_LOCATION$CACHE_PATH" "$CACHE_PATH"
#Restart plex echo "Starting Plex" systemctl start "$SERVICE_NAME"
2
« on: September 15, 2024, 10:24:45 pm »
The below commands are tailored for Linux, as file paths are different for Windows. They need to be ran with the sudo -u plex prefix for proper user access permissions. Get list of libraries'/usr/lib/plexmediaserver/Plex Media Scanner' --list Force library intro dection'/usr/lib/plexmediaserver/Plex Media Scanner' --analyze --manual --server-action intros --section LIBRARY_ID Force library credit dection'/usr/lib/plexmediaserver/Plex Media Scanner' --analyze --manual --server-action credits --section LIBRARY_ID Export media list sqlite3 '/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db' ' SELECT MDI.id, file, title, MDI."index", hints, GROUP_CONCAT(T.text) AS taggings FROM media_parts AS MP INNER JOIN media_items AS MI ON MI.id=MP.media_item_id INNER JOIN metadata_items AS MDI ON MDI.id=MI.metadata_item_id LEFT JOIN taggings as T ON T.metadata_item_id=MDI.id AND T.text IN ("credits", "intro") GROUP BY MDI.id'
This outputs the following (column separated by pipe “|”) for each individual media item: - Metadata item ID
- File path
- Title
- Episode Number
- Hints (e.x. =&episode=1&episodic=1&season=1&show=Ninja%20Turtles&show_name=Ninja%20Turtles&year=2002)
- “credits” and “intros” tags (if the credits and intros timestamps have been calculated)
3
« on: July 30, 2024, 04:46:45 pm »
Yesterday I moved my Plex installation from a Windows machine to a Linux machine. The primary data folders that needed to be copied over were Media, Metadata, Plug-ins, and Plug-in Support. It doesn't hurt to copy over some of the folders in Cache too. It's possible there may be some more data that needs to be moved over, but I don't have documented what. After moving all the data, I updated the paths in the database for the new machine. Doing this allowed me to keep everything as it was and no new refreshes/scans needed to be done. The location of the Plex modified SQLite for me was at /usr/lib/plexmediaserver/Plex SQLite. So the following is the bash commands to stop the plex server and open SQLite editor on Linux Mint 21.3. service plexmediaserver stop /usr/lib/plexmediaserver/Plex\ SQLite '/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db'
And the following is the SQL I used to replace a D: drive with a path of /home/plex/drives/d/. You can replace these strings in the below code with your custom drives and paths. #Replace backslashes with forward slash in paths UPDATE media_parts SET file=REPLACE(file, '\', '/'); UPDATE section_locations SET root_path=REPLACE(root_path, '\', '/'); UPDATE media_streams SET url=REPLACE(url, '\', '/') WHERE url LIKE 'file://%';
#Replace root paths UPDATE media_parts SET file=REPLACE(file, 'D:/', '/home/plex/drives/d/'); UPDATE section_locations SET root_path=REPLACE(root_path, 'D:/', '/home/plex/drives/d/'); UPDATE media_streams SET url=REPLACE(url, 'file://D:/', 'file:///home/plex/drives/d/') WHERE url LIKE 'file://%'; UPDATE media_streams SET url=REPLACE(url, 'file:///D:/', 'file:///home/plex/drives/d/') WHERE url LIKE 'file://%'; UPDATE metadata_items SET guid=REPLACE(guid, 'file:///D:/', 'file:///home/plex/drives/d/');
4
« on: July 19, 2024, 03:57:03 am »
I created this script because I wasn’t happy with the native Roboform importer inside Bitwarden. This fixed multiple problems including: * Ignoring MatchUrls * Parent folders weren’t created if they had no items in them (e.x. if I had identities in Financial/Banks, but nothing in /Financial, then it wouldn’t be created /Financial) * Completely ignoring the extra fields (RfFieldsV2)
This fixes all those problems.
This needs to be ran via php in a command line interface, so `php convert.php`. It requires the [bitwarden cli “bw” command](https://bitwarden.com/help/cli/). There are 2 optional arguments you can pass: 1) The name of the import file. If not given, `./RfExport.csv` will be used 2) The “bw” session token. If not given as a parameter, it can be set directly inside this file on line #4. You get this token by running `bw unlock` (after logging in with `bw login`).
This script does the following: * Reads from a csv file exported by Roboform to import into bitwarden * Runs `bw sync` before processing * Imported Fields: * Name: Becomes the name of the item * Url: Becomes the URL for the item * MatchUrl: If this is not the same “Url” then it is added as another URL for the item * Login: Becomes the login (user) name * Pwd: Becomes the password * Note: Becomes the note * Folder: Item is put in this folder * RfFieldsV2 (multiple): * Fields that match the login or password fields are marked as “linked fields” to those. If the field names are the following, they are not added as linked fields: *user, username, login, email, userid, user id, user id$, user_email, login_email, user_login, password, passwd, password$, pass, user_password, login_password, pwd, loginpassword* * Fields that are different than the login or password are added as appropriate “Custom fields”. Supported types: '', rad, sel, txt, pwd, rck, chk, are * Each field has 5 values within it, and 3 of those seem to be different types of names. So as long as they are not duplicates of each other, each name is stored as a seperate field. * If all fields are blank but “name” and “note” then the item is considered a “Secure Note” * While reading the csv file for import, errors and warnings are sent to stdout * After the csv import has complete, it will give a total number of warnings/errors and ask the user if they want to continue * Creates missing folders (including parents that have no items in them) * During export to bitwarden: * This process can be quite slow since each instance of running “bw” has to do a lot of work * Keeps an active count of items processed and the total number of items * If duplicates are found (same item name and folder) then the user is prompted for what they want to do. Which is either s=skip or o=overwrite. A capitol of those letters can be given to perform the same action on all subsequent duplicates. * Errors are sent to stdout
Download script here <?php global $BW_SESSION; /** @noinspection SpellCheckingInspection,RedundantSuppression */ $BW_SESSION=($argv[2] ?? 'FILL_ME_IN');
print RunMe($argv)."\n";
//Make sure “bw” is installed, given session is valid, and is synced function CheckBWStatus(): ?string { if(is_string($StatusCheck=RunBW('status'))) return $StatusCheck; else if(!is_object($StatusCheck) || !isset($StatusCheck->status)) return 'bw return does not have status'; else if($StatusCheck->status==='locked') return 'bw is locked'; else if($StatusCheck->status!=='unlocked') return 'bw status is invalid';
$ExpectedMessage='Syncing complete.'; if(is_string($SyncCheck=RunBW('sync', null, false))) return $SyncCheck; else if($SyncCheck[0]!==$ExpectedMessage) return "Sync expected “${ExpectedMessage}” but got “${SyncCheck[0]}”";
return null; }
//Pull the known folders function PullKnownFolders(): string|array { $FolderIDs=[]; if(is_string($FolderList=RunBW('list folders'))) return $FolderList; foreach($FolderList as $Folder) $FolderIDs[$Folder->name]=$Folder->id; unset($FolderIDs['No Folder']); return $FolderIDs; }
//Get the file handle and the column indexes function GetFileAndColumns(): string|array { //Prepare the import file for reading ini_set('default_charset', 'UTF-8'); $FileName=$argv[1] ?? './RfExport.csv'; if(!file_exists($FileName)) return 'File not found: '.$FileName; else if(!($f=fopen($FileName, 'r'))) return 'Error opening file: '.$FileName;
//Check the header row values if(!($HeadRow=fgetcsv($f))) return 'Error opening file: '.$FileName; else if(!count($HeadRow) || $HeadRow[0]===NULL) return 'Missing head row: '.$FileName; if(str_starts_with($HeadRow[0], "\xEF\xBB\xBF")) //Remove UTF8 BOM $HeadRow[0]=substr($HeadRow[0], 3); unset($FileName);
$ExpectedCols=array_flip(['Url', 'Name', 'MatchUrl', 'Login', 'Pwd', 'Note', 'Folder', 'RfFieldsV2']); $ColNums=[]; foreach($HeadRow as $Index => $Name) { if(!isset($ExpectedCols[$Name])) if(isset($ColNums[$Name])) return 'Duplicate column title: '.$Name; else return 'Unknown column title: '.$Name; $ColNums[$Name]=$Index; unset($ExpectedCols[$Name]); } if(count($ExpectedCols)) return 'Required columns not found: '.implode(', ', array_keys($ExpectedCols)); else if($ColNums['RfFieldsV2']!==count($ColNums)-1) return 'RfFieldsV2 must be the last column';
return [$f, $ColNums]; }
//Process the rows function ProcessRows($f, array $ColNums, array &$FolderIDs): array { $Counts=['Error'=>0, 'Warning'=>0, 'Success'=>0]; $RowNum=0; $FolderNames=[]; $Items=[]; while($Line=fgetcsv($f, null, ",", "\"", "")) { //Process the row and add result type to counts $RowNum++; $Result=ProcessRow($Line, $ColNums); $Counts[$Result[0]]++;
//Handle errors and warnings if($Result[0]!=='Success') { print "$Result[0]: Row #$RowNum $Result[1]\n"; continue; } else if(isset($Result['Warnings']) && count($Result['Warnings'])) { print "Warning(s): Row #$RowNum ".implode("\n", $Result['Warnings'])."\n"; $Counts['Warning']+=count($Result['Warnings']); }
//Add the folder to the list of folders (strip leading slash) $FolderName=($Line[$ColNums['Folder']] ?? ''); $FolderName=substr($FolderName, ($FolderName[0] ?? '')==='/' ? 1 : 0); $Result['Data']->folderId=&$FolderIDs[$FolderName]; $FolderNames[$FolderName]=1;
//Save the entry $Items[]=$Result['Data']; } fclose($f); return [$Counts, $FolderNames, $Items, $RowNum]; }
//Process a single row function ProcessRow($Line, $ColNums): array { //Skip blank lines if(!count($Line) || (count($Line)===1 && $Line[0]===null)) return ['Warning', 'is blank'];
//Extract the columns by name $C=(object)[]; foreach($ColNums as $Name => $ColNum) $C->$Name=$Line[$ColNum] ?? '';
//Check for errors and end processing early for notes if($C->Name==='') return ['Error', 'is missing a name']; else if($C->Url==='' && $C->MatchUrl==='' & $C->Login==='' && $C->Pwd==='') { return ['Success', 'Data'=>(object)['type'=>2, 'notes'=>$C->Note, 'name'=>$C->Name, 'secureNote'=>['type'=>0]]]; }
//Create a login card $Ret=(object)[ 'type'=>1, 'name'=>$C->Name, 'notes'=>$C->Note==='' ? null : $C->Note, 'login'=>(object)[ 'uris'=>[(object)['uri'=>$C->Url]], 'username'=>$C->Login, 'password'=>$C->Pwd, ], ]; if($C->MatchUrl!==$C->Url) $Ret->login->uris[]=(object)['uri'=>$C->MatchUrl];
//Create error string for mist fields $i=0; //Declared up here so it can be used in $FormatError $FormatError=function($Format, ...$Args) use ($ColNums, &$i): string { return 'RfFieldsV2 #'.($i-$ColNums['RfFieldsV2']+1).' '.sprintf($Format, ...$Args); };
//Handle misc/extra (RfFieldsV2) fields $Warnings=[]; for($i=$ColNums['RfFieldsV2']; $i<count($Line); $i++) { //Pull in the parts of the misc field if(count($Parts=str_getcsv($Line[$i], ",", "\"", ""))!==5) return ['Error', $FormatError('is not in the correct format')]; $Parts=(object)array_combine(['Name', 'Name3', 'Name2', 'Type', 'Value'], $Parts); if(!trim($Parts->Name)) return ['Error', $FormatError('invalid blank name found')];
//Figure out which “Names” to process $PartNames=[trim($Parts->Name)]; foreach([$Parts->Name2, $Parts->Name3] as $NewName) if($NewName!=='' && !in_array($NewName, $PartNames)) $PartNames[]=$NewName;
//Process the different names foreach($PartNames as $PartName) { //Determined values for the item $LinkedID=null; $PartValue=$Parts->Value; /** @noinspection PhpUnusedLocalVariableInspection */ $Type=null; //Overwritten in all paths
//Handle duplicate usernames and password fields if(($IsLogin=($PartValue===$C->Login)) || $PartValue===$C->Pwd) { if($Parts->Type!==($IsLogin ? 'txt' : 'pwd')) $Warnings[]=$FormatError('expected type “%s” but got “%s”', $IsLogin ? 'txt' : 'pwd', $Parts->Type); $Type=3; $PartValue=null; $LinkedID=($IsLogin ? 100 : 101);
//If a common name then do not add the linked field /** @noinspection SpellCheckingInspection */ if(in_array( strToLower($PartName), $IsLogin ? ['user', 'username', 'login', 'email', 'userid', 'user id', 'user id$', 'user_email', 'login_email', 'user_login'] : ['password', 'passwd', 'password$', 'pass', 'user_password', 'login_password', 'pwd', 'loginpassword'] )) continue; } else { //Convert the type switch($Parts->Type) { case '': //For some reason some text fields have no type given case 'rad': case 'sel': case 'txt': $Type=0; break; case 'pwd': $Type=1; break; case 'rck': //Radio check? $Type=0; if($PartName!==$Parts->Name) //Ignore second names for radio checks continue 2; if(count($RadioParts=explode(':', $PartName))!==2) return ['Error', $FormatError('radio name needs 2 parts separated by a colon')]; $PartName=$RadioParts[0]; $PartValue=$RadioParts[1]; break; case 'chk': $Type=2; if($PartValue==='*' || $PartValue==='1') $PartValue='true'; else if($PartValue==='0') $PartValue='false'; else return ['Error', $FormatError('invalid value for chk type “%s”', $PartValue)]; break; case 'are': //This seems to be a captcha continue 2; default: return ['Error', $FormatError('invalid field type “%s”', $Parts->Type)]; } }
//Create the return object if(!isset($Ret->fields)) $Ret->fields=[]; $Ret->fields[]=(object)[ 'name'=>$PartName, 'value'=>$PartValue, 'type'=>$Type, 'linkedId'=>$LinkedID, ]; } }
//Return finished item and warnings if(count($Warnings)) $Warnings[0]="RfFieldsV2 warnings:\n".$Warnings[0]; return ['Success', 'Data'=>$Ret, 'Warnings'=>$Warnings]; }
//Ask the user if they want to continue function ConfirmContinue($Counts, $RowNum): ?string { //Ask the user if they want to continue printf("Import from spreadsheet is finished. Total: %d; Successful: %d, Errors: %d, Warnings: %d\n", $RowNum, $Counts['Success'], $Counts['Error'], $Counts['Warning']); while(1) { print 'Do you wish to continue the export to bitwarden? (y/n): '; fflush(STDOUT); switch(trim(strtolower(fgets(STDIN)))) { case 'n': return 'Exiting'; case 'y': break 2; } }
return null; }
//Get folder IDs and create parent folders for children function GetFolders($FolderNames, &$FolderIDs): ?string { foreach(array_keys($FolderNames) as $FolderName) { //Skip “No Folder” if($FolderName==='') { $FolderIDs[$FolderName]=''; continue; }
//Check each part of the folder tree to make sure it exists $FolderParts=explode('/', $FolderName); $CurPath=''; foreach($FolderParts as $Index => $FolderPart) { $CurPath.=($Index>0 ? '/' : '').$FolderPart;
//If folder is already cached then nothing to do if(isset($FolderIDs[$CurPath])) { continue; }
//Create the folder print "Creating folder: $CurPath\n"; if(is_string($FolderInfo=RunBW('create folder '.base64_encode(json_encode(['name'=>$CurPath])), 'create folder: '.$CurPath))) return $FolderInfo; else if (!isset($FolderInfo->id)) return "bw folder create failed for “${CurPath}”: Return did not contain id"; $FolderIDs[$CurPath]=$FolderInfo->id; } } return null; }
//Pull the known items function PullKnownItems($FoldersByID): string|array { $CurItems=[]; if(is_string($ItemList=RunBW('list items'))) return $ItemList; foreach($ItemList as $Item) { $CurItems[($Item->folderId===null ? '' : $FoldersByID[$Item->folderId].'/').$Item->name]=$Item->id; } return $CurItems; }
function StoreItems($Items, $CurItems, $FoldersByID): int { print "\n"; //Give an extra newline before starting the processing $NumItems=count($Items); $NumErrors=0; $HandleDuplicatesAction=''; //The "always do this" action foreach($Items as $Index => $Item) { //Clear the current line and print the processing status printf("\rProcessing item #%d/%d", $Index+1, $NumItems); fflush(STDOUT);
//If the item already exists then request what to do $FullItemName=($Item->folderId===null ? '' : $FoldersByID[$Item->folderId].'/').$Item->name; if(isset($CurItems[$FullItemName])) { //If no "always" action has been set in $HandleDuplicatesAction then ask the user what to do if(!($Action=$HandleDuplicatesAction)) { print "\n"; while(1) { print "A duplicate at “${FullItemName}” was found. Choose your action (s=skip,o=overwrite,S=always skip,O=always overwrite):"; fflush(STDOUT); $Val=trim(fgets(STDIN)); if(in_array(strtolower($Val), ['s', 'o'])) break; } $Action=strtolower($Val); if($Val!==$Action) //Upper case sets the "always" state $HandleDuplicatesAction=$Action; }
//Skip the item if($Action==='s') continue;
//Overwrite the item if(is_string($Ret=RunBW('edit item '.$CurItems[$FullItemName].' '.base64_encode(json_encode($Item)), 'edit item: '.$FullItemName))) { $NumErrors++; printf("\nError on item #%d: %s\n", $Index+1, $Ret); } continue; }
//Create the item if(is_string($Ret=RunBW('create item '.base64_encode(json_encode($Item)), 'create item: '.$FullItemName))) { $NumErrors++; printf("\nError on item #%d: %s\n", $Index+1, $Ret); } } return $NumErrors; }
//Run a command through the “bw” application function RunBW($Command, $Label=null, $DecodeJSON=true): string|object|array { //$Label is set to $Command if not given if($Label===null) $Label=$Command;
//Run the command and check the results global $BW_SESSION; exec(sprintf('BW_SESSION=%s bw %s --pretty 2>&1', $BW_SESSION, $Command), $Output, $ResultCode); if($ResultCode===127) return 'bw is not installed'; else if($ResultCode===1) return "bw “${Label}” threw an error: \n".implode("\n", $Output); else if($ResultCode!==0) return "bw “${Label}” returned an invalid status code [$ResultCode]: \n".implode("\n", $Output); else if($Output[0]==='mac failed.') return 'Invalid session ID'; else if(!$DecodeJSON) return [implode("\n", $Output)]; else if(($JsonRet=json_decode(implode("\n", $Output)))===null) return "bw “${Label}” returned non-json result";
//Return the json object return $JsonRet; }
function RunMe($argv): string { //Make sure “bw” is installed and given session is valid if(is_string($Ret=CheckBWStatus())) return $Ret;
//Pull the known folders if(is_string($FolderIDs=PullKnownFolders())) return $FolderIDs;
//Get the file handle and the column indexes if(is_string($Ret=GetFileAndColumns())) return $Ret; [$f, $ColNums]=$Ret; unset($argv);
//Process the rows and ask the user if they want to continue [$Counts, $FolderNames, $Items, $RowNum]=ProcessRows($f, $ColNums, $FolderIDs); if(is_string($Ret=ConfirmContinue($Counts, $RowNum))) return $Ret; unset($Counts, $RowNum, $f, $ColNums);
//Get folder IDs and create parent folders for children if(is_string($Ret=GetFolders($FolderNames, $FolderIDs))) return $Ret; unset($FolderNames);
//Pull the known items $FoldersByID=array_flip($FolderIDs); if(is_string($CurItems=PullKnownItems($FoldersByID))) return $CurItems;
//Store all items $FolderIDs['']=null; //Change empty folder to id=null for json insertions $NumErrors=StoreItems($Items, $CurItems, $FoldersByID);
//Return completion information return "\nCompleted ".($NumErrors ? "with $NumErrors error(s)" : 'successfully'); }
5
« on: July 12, 2024, 10:36:53 pm »
Since I have been moving my primary workstation to Linux, I have been in need of finding a new password manager after Roboform failed me. I decided to try out KeePassXC but was having trouble since my Ungoogled Chromium browser was sandboxed in Flatpak. The instructions for this on Unix Stack Exchange were no longer working so I had to dig into it myself. The 2 primary problems were that: 1) The browsers’ sandboxes did not have the QT libs. 2) The interprocess communication pipe socket had been renamed from kpxc_server to org.keepassxc.KeePassXC.BrowserServer. The following are the instructions to get KeePassXC running in Flatpak versions of both Chrome and Firefox. This was tested on Linux Mint 21.3 with both Ungoogled Chromium and Firefox. You will need to change the KP_FLATPAK_PACKAGE if you use other versions of Chrome. - Run the relevant below environment variables in your command shell before running the commands in the following steps:
#Shared environment variables: KP_CUSTOM=/home/$USER/keepass-browser KP_JSON_NAME=org.keepassxc.keepassxc_browser.json
#Chrome environment variables: KP_FLATPAK_PACKAGE=io.github.ungoogled_software.ungoogled_chromium KP_JSON_START=~/.config/chromium/NativeMessagingHosts KP_JSON_END=~/.var/app/$KP_FLATPAK_PACKAGE/config/chromium/NativeMessagingHosts
#Firefox environment variables: KP_FLATPAK_PACKAGE=org.mozilla.firefox KP_JSON_START=~/.mozilla/native-messaging-hosts KP_JSON_END=~/.var/app/$KP_FLATPAK_PACKAGE/.mozilla/native-messaging-hosts
- Install and enable the browser extension:
KeePassXD > Tools > Settings > Browser Integration:
- Check “Enable Browser Integration”
- Check “Chromium” and/or “Firefox”
- Download the plugin listed on this screen in your browser
- Click "OK"
Note: This creates $KP_JSON_START/$KP_JSON_NAME
- Set up the needed files in the sandbox:
#Put KeePass proxy and needed library files in user directory mkdir -p $KP_CUSTOM/lib mkdir -p $KP_JSON_END #Needed for firefox cp /usr/bin/keepassxc-proxy $KP_CUSTOM/ rsync -a /usr/lib/x86_64-linux-gnu/libicudata* /usr/lib/x86_64-linux-gnu/libicuuc* /usr/lib/x86_64-linux-gnu/libicui* /usr/lib/x86_64-linux-gnu/libdouble* /usr/lib/x86_64-linux-gnu/libsodium* /usr/lib/x86_64-linux-gnu/libQt5* $KP_CUSTOM/lib
#Copy the JSON file to the Flatpak app directory and change the executable path in the file cp $KP_JSON_START/$KP_JSON_NAME $KP_JSON_END/ sed -i "s/\/usr\/bin\//"$(echo $KP_CUSTOM | sed 's_/_\\/_g')"\//" $KP_JSON_END/$KP_JSON_NAME
- Add permissions to the Flatpak:
flatpak override --user --filesystem=$KP_CUSTOM:ro $KP_FLATPAK_PACKAGE #Only required if home directory is not shared to the Flatpak flatpak override --user --filesystem=xdg-run/org.keepassxc.KeePassXC.BrowserServer:ro $KP_FLATPAK_PACKAGE flatpak override --user --env=LD_LIBRARY_PATH=$(flatpak info --show-permissions $KP_FLATPAK_PACKAGE | grep -oP '(?<=LD_LIBRARY_PATH=).*')";$KP_CUSTOM/lib" $KP_FLATPAK_PACKAGE
6
« on: July 04, 2024, 01:37:42 am »
Part 0: Installation I’ve been moving from Windows to Linux recently and my latest software move attempt is Roboform, a password manager that I use in offline mode. Just running it under Wine works fine for the primary software interaction, but I was unable to get it fully integrated with its chrome extension. Below is the information for my attempt to get it working, in case someone could use the information or wanted to try continuing my attempts. To move your RoboForm profile to Linux, copy the data from C:\Users\USERNAME\AppData\Local\RoboForm\Profiles\Default Profile to ~/.wine/drive_c/users/USERNAME/Local Settings/Application Data/RoboForm/Profiles/Default Profile. Part 1: Redirect the extension to the executable The chrome extension talks to its parent, rf-chrome-nm-host.exe, through the native messaging API. To direct the extension to talk to the windows executable you have to edit ~/.config/chromium/NativeMessagingHosts/com.siber.roboform.json and change the path inside it to /home/USERNAME/chrome-robo.sh, a script file that you will create. You can’t link directly to the rf-chrome-nm-host.exe because it has to be run through Wine, and the path cannot contain arguments. Create a file with executable permissions at ~/chrome-robo.sh and set its contents to: cd "/home/USERNAME/.wine/drive_c/Program Files (x86)/Siber Systems/AI RoboForm/9.6.1.1/"; /usr/bin/wine ./rf-chrome-nm-host.exe chrome-extension://pnlccmojcmeohlpggmfnbbiapkmbliob/ --parent-window=0
Part 2: Debugging why it isn’t working It should have worked at this point, but it still wasn’t, so I had to go further into debug mode. The full copy of the C source code can be found at the bottom of this post. Make sure to replace USERNAME in the source (and instructions in this post) with your username, as using “~” to specify your home directory often doesn’t work in this setup. You may also need to replace version numbers (9.6.1.1 for this post). First, I created a simple C program to sit in between the chrome extension and the rf-chrome-nm-host.exe. All it did was forward the stdin/stdout between both programs (left=chromium, right=rf-chrome-nm-host.exe) and output their crosstalk to a log file (./log) that I monitored with tail -f. I pointed to the generated executable in the ~/chrome-robo.sh file. All that was generated on the Linux config was: left=ping, right=confirm ping, left=get product info, END. I then modified the program to specifically handle chrome native messaging packets, which are always a 4 byte number specifying the packet length, followed by the packet data (IsChrome=1). If this variable is turned off there is a place in the code where you can set a different executable with parameters to run. Next, I ran the program in Windows with a working left+right config so I could see what packets were expected. I then added a hack to the program (AddRoboformAnswerHacks=1) to respond to the 2nd-4th packets sent from the left (get-product-info, get-product-info, Initialize2) with what was expected from the right. rf-chrome-nm-host.exe crashed on the 5th packet, and debugging further from there would have taken too more time than I was willing to put in, so at that point I gave up. Part 3: Trying with windows I next decided to see if I could get the chromium extension to play nicely by talking directly to the rf-chrome-nm-host.exe on my Windows machine via an SSH tunnel. To do this, I changed the char *cmd[]= line to: char *cmd[]={ "/usr/bin/ssh", "USERNAME@HOSTNAME", "cd c:/Program\\ Files\\ \\(x86\\)/Siber\\ Systems/AI\\ RoboForm/9.6.1.1; ./rf-chrome-nm-host.exe chrome-extension://pnlccmojcmeohlpggmfnbbiapkmbliob/ --parent-window=0", NULL };
While the first 4 packets succeeded in this setup, the following packets (rf-api-request) were all met with: pipe open error: The system cannot find the file specified. (error 2). This was another stopping point because debugging this would also have taken too long. Though I did do some initial testing using Process Monitor and handle lookups in Process Explorer. Part 4: The C source code #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <string.h>
FILE *SideLog; //This logs all events to the file "log" FILE *RecordLeft; //This logs the exact packets that the left side sends to the file "left" int IsChrome=1; //True for roboform chrome compatibility, false to just pass through data as is int DisplayStatus=1; //Whether to show the "Status: x" updates int AddRoboformAnswerHacks=0; //Whether to force send fake responses that the rf-chrome server is not responding to const char* HomePath="/home/USERNAME/.wine/drive_c/Program Files (x86)/Siber Systems/AI RoboForm/9.6.1.1/"; //This must be set. const char* ExePath="/home/USERNAME/.wine/drive_c/Program Files (x86)/Siber Systems/AI RoboForm/9.6.1.1/rf-chrome-nm-host.exe"; //This must be set. You can make sure rf-chrome-nm-host.exe is running with a 'ps'
//Send back a custom packet to the left. This will show in the log file BEFORE the packet it is responding to. void SendLeft(char *str) { char Buffer[255]; int StrSize=strlen(str); *(int*)Buffer=StrSize; strcpy(Buffer+4, str);
write(STDOUT_FILENO, Buffer, StrSize+4); fprintf(SideLog, "OVERRIDE REQUEST: %s\n\n", Buffer+4); fflush(SideLog); }
//Forward data from left to right or vice versa. Also saves to logs and "left" file void ForwardSide(int InFileHandle, int OutFileHandle, fd_set* ReadFDS, int IsLeft) { //Exit here if no data if(!FD_ISSET(InFileHandle, ReadFDS)) return;
//Create a static 1MB+1K buffer - Max packet with chrome is 1MB const int BufferLen=1024*(1024+1); static char* Buffer=0; if(!Buffer) Buffer=malloc(BufferLen);
//If not chrome, just pass the data as is char* Side=(IsLeft ? "left" : "right"); if(!IsChrome) { int ReadSize=read(InFileHandle, Buffer, BufferLen); write(OutFileHandle, Buffer, ReadSize); if(IsLeft) fwrite(Buffer, ReadSize, 1, RecordLeft); Buffer[ReadSize]=0; fprintf(SideLog, "%s (%d): %s\n\n", Side, ReadSize, Buffer); fflush(SideLog); return; }
//Read the 4 byte packet size and store it at the beginning of the buffer unsigned int PacketSize; read(InFileHandle, &PacketSize, 4); *(unsigned int*)Buffer=PacketSize;
//Read in the packet and zero it out at the end for the string functions read(InFileHandle, Buffer+4, PacketSize); Buffer[PacketSize+4]=0;
//Send fake product-info packet since rf-chrome-nm-host.exe was not responding to it if(AddRoboformAnswerHacks && IsLeft && strstr(Buffer+4, "\"name\":\"getProp\"") && strstr(Buffer+4, "\"args\":\"product-info\"")) { //The return packet with header at the front char VersionInfo[]="{\"callbackId\":\"2\",\"result\":\"{\\\"version\\\":\\\"9-6-1-1\\\",\\\"haveReportAnIssue\\\":true,\\\"haveBreachMon\\\":true,\\\"haveLoginIntoAccount\\\":true}\"}";
//Simplistic version counter hack since chrome always sends 2 version info requests at the beginning static int VersionCount=2; VersionInfo[15]='0'+VersionCount; VersionCount++;
SendLeft(VersionInfo); //Send fake initialization info packet since rf-chrome-nm-host.exe was not responding to it } else if(AddRoboformAnswerHacks && IsLeft && strstr(Buffer+4, "\"name\":\"Initialize2\"")) { SendLeft("{\"callbackId\":\"4\",\"result\":\"rf-api\"}"); //Forward the packet to the other side and store in the "left" file if left side } else { write(OutFileHandle, Buffer, PacketSize+4); if(IsLeft) fwrite(Buffer, PacketSize+4, 1, RecordLeft); }
//Output the packet to the log fprintf(SideLog, "%s (%d): %s\n\n", Side, PacketSize, Buffer+4); fflush(SideLog); }
int main(void) { //Create pipes int pipe1[2]; //Parent writes to child int pipe2[2]; //Child writes to parent if(pipe(pipe1)==-1 || pipe(pipe2)==-1) { perror("pipe"); exit(EXIT_FAILURE); }
//Fork the current process pid_t pid = fork(); if(pid==-1) { perror("fork"); exit(EXIT_FAILURE); }
//New (child) process if(pid == 0) { //Close unused ends of the pipes close(pipe1[1]); // Close write end of pipe1 close(pipe2[0]); // Close read end of pipe2
//Redirect stdin to the read end of pipe1 dup2(pipe1[0], STDIN_FILENO); close(pipe1[0]);
//Redirect stdout to the write end of pipe2 dup2(pipe2[1], STDOUT_FILENO); close(pipe2[1]);
//Move to the roboform home directory if(IsChrome) { if(chdir(HomePath) == -1) { perror("chdir"); exit(EXIT_FAILURE); } }
//Execute a command that reads from stdin and writes to stdout. The default is the chrome command. If not in chrome, you can fill in the exe and parameter you wish to use char *cmd[] = {"/usr/bin/wine", (char*)ExePath, "chrome-extension://pnlccmojcmeohlpggmfnbbiapkmbliob/", "--parent-window=0", NULL}; if(!IsChrome) { cmd[0]="/usr/bin/php"; cmd[1]="echo.php"; } execvp(cmd[0], cmd); perror("execlp"); exit(EXIT_FAILURE); }
//---Parent process - forwards both sides--- //Close unused ends of the pipes close(pipe1[0]); // Close read end of pipe1 close(pipe2[1]); // Close write end of pipe2
//Open the log files SideLog = fopen("./log", "w+"); RecordLeft = fopen("./left", "w+");
//Run the main loop int max_fd=pipe2[0]+1; //Other pipe is STDIN which is 0 while(1) { //Create the structures needed for select fd_set read_fds; FD_ZERO(&read_fds); FD_SET(STDIN_FILENO, &read_fds); FD_SET(pipe2[0], &read_fds);
struct timeval timeout; timeout.tv_sec = 10; timeout.tv_usec = 0; //Listen for an update int status = select(max_fd, &read_fds, NULL, NULL, &timeout);
//Display "Status: x" if its setting is true if(DisplayStatus) { fprintf(SideLog, "Status: %d\n", status); if(status==0) fflush(SideLog); }
//Exit on bad status if (status==-1) { perror("select"); break; }
//Check both sides to see if they need to forward a packet ForwardSide(STDIN_FILENO, pipe1[1], &read_fds, 1); ForwardSide(pipe2[0], STDOUT_FILENO, &read_fds, 0); }
//Close pipes close(pipe1[1]); close(pipe2[0]);
//Wait for the child process to finish wait(NULL);
return EXIT_SUCCESS; }
7
« on: May 02, 2024, 10:33:44 pm »
After years of saying I’d do it, I'm finally moving my ecosystem from Windows to Linux. After some experimenting and soul searching, I've decided to go with Linux Mint, as it's not exactly Ubuntu with their atrociously horrible decisions, but it provides stability, ease of setup, and a similar enough interface to Windows so as to not lower my productivity. Getting all of my legacy hardware working, including my 6-monitor setup, was mostly painless, but my beloved Babyface Pro (external professional audio mixer hardware) has been absolute hell to get working. It only natively supports Windows, OSX, and iOS for the “PC” (USB audio passthrough) mode, and the CC mode (class compliant) does not offer my custom waveform transforms. So, my only real option was to use the other analog input interfaces on the Babyface (XLR, SPDIF, or quarter inch audio). The first hurdle was the power. Normally the device is powered through USB, however if I was going to be using the other audio inputs, I didn't want to leave the USB plugged in all the time, and the Babyface doesn't come with a power adapter. Fortunately, I had a 12V 1A+ power adapter in my big box of random power adapters. The second hurdle was when I discovered that the Babyface does not store the mixer settings when it's powered off. So, every time it gets powered on, it needs to be hooked to another (Windows) machine that can push the mixer settings to it. This also isn't too big a deal as I keep it on a UPS so it generally won't lose power, and if it does, I can use a VM to push the settings. The next problem was deciding what interface to go through. I was really hoping to use SPDIF/optical since it is a digital signal that does not suffer from interference and degradation, but all the SPDIF interfaces I tried (4 in total) all sounded like garbage. I guess the SPDIF interface on the Babyface is a piece of Junk, which was very disheartening. My only remaining option was using the analog inputs. I decided to use a mini (3.5mm; 1/8") stereo to quarter inch (6.35mm) mono splitter cord to run into “IN 3/4” and this worked perfectly. However, if the USB interface is plugged in at the same time then this actually creates very audible line noise on the analog inputs within the Babyface itself! This is a horrible design flaw of the device that I was shocked to run into. Fortunately, as mentioned in step 1, I already planned on having the USB cord unplugged, so not a deal breaker. I first tried the headphone and line out jacks on my motherboard, but the audio quality was only at about 90%. I next tried the line out on my Creative Sound Blaster Audigy from 2014 and the audio was at about 95% quality. It also felt like a cardinal sin to plug in a PCIE 1.0 1x device (0.250 GB/s) into a PCIE 5.0 16x slot (63 GB/s) - lol. So, I bought a Sound Blaster Play 3! USB to mini audio adapter and the audio was perfect! I finally had my setup figured out. As a fun note, I went to an audiologist a few days ago to have my hearing tested, and the waveform I had devised (through brute force testing) that I had been using through the Babyface for the last 7 years was the exact inverse of the results on the hearing loss frequency chart.
8
« on: April 14, 2024, 11:11:15 pm »
The nulltypes system has been overhauled so all null types are under a generic type named NullType in the top level package. For example, instead of using nulltypes.NullUint8 you would now use NullType[uint8]. This also really helped clean up the null types code. This is a version breaking change, hence the minor version number update. Other minor changes: - Readme file and package information has been updated with the following changes:
- The type support section has been redone for clarity
- The structs in the code examples have had the members labeled to explain their used supported type
- Marshled JSON strings are now properly json escaped
- Added bypass for my RawBytes bug fix, now that it has been fixed in go v1.23
- Removed test case that is no longer compatible with go 1.21+
9
« on: March 23, 2024, 08:44:56 am »
I made a pretty massive overhaul of my resume. Both the PDF and the copy on this site’s resume section have been updated.
10
« on: March 16, 2024, 03:08:08 am »
GoFasterSQL is a tool designed to enhance the efficiency and simplicity of scanning SQL rows into structures in Go[lang]. While this project was first released in December of 2023 on github and has already had 7 releases there, I’ve finally gotten around to adding it here. I don’t anticipate there being many more releases any time soon as the project is feature complete.
11
« on: March 16, 2024, 02:50:14 am »
Description: GoFasterSQL is a tool designed to enhance the efficiency and simplicity of scanning SQL rows into structures. Information: The flaw in the native library scanning process is its repetitive and time-consuming type determination for each row scan. It must match each field’s type with its native counterpart before converting the data string (byte array). Furthermore, the requirement to specify each individual field for scanning is tedious. GoFasterSQL instead precalculates string-to-type conversions for each field, utilizing pointers to dedicated conversion functions. This approach eliminates the need for type lookups during scanning, vastly improving performance. The library offers a 2 to 2.5 times speed increase compared to native scan methods (5*+ vs sqlx), a boost that varies with the number of items in each scan. Moreover, its automatic structure determination feature is a significant time-saver during coding. See the github page for full documentation. Languages: MySQL, GoLang
12
« on: March 16, 2024, 02:33:50 am »
Gol10n is a highly space and memory optimized l10n (localization) library for Go (GoLang) pronounced “Goal Ten”. While this project was first released in December of 2023 on github and has already had 5 releases there, I’ve finally gotten around to adding it here. I don’t anticipate there being many more releases any time soon as the project is feature complete.
13
« on: March 16, 2024, 02:00:13 am »
Description: This is a highly space and memory optimized l10n (localization) library for Go (GoLang) pronounced “Goal Ten”. Information: Translation strings are held, per language, in text files (either YAML or JSON), and compile into .gtr or .gtr.gz (gzip compressed) files. Translations can be referenced in Go code either by an index, or a namespace and translation ID. Referencing by index is the fastest, most efficient, and what this library was built for. Indexes are stored as constants in generated Go dictionary files by namespace, and are also held in the dictionary. See the github page for full documentation. Languages: GoLang
14
« on: March 16, 2024, 12:15:03 am »
I finally got around to bringing this ~20 year old website somewhat up to date. - Hid ~10 old projects that noone would care about or are silly to download an executable for when a website could just as easily handle it.
- Redid the projects page to list projects by type, cleaned up the layout, and removed the silly sorting and smart table stuff.
- Changed background to a solid color instead of 1px stripes, which didn’t work well on mobile.
- Fixed layout issues with dynamic section borders
- Fixed other layout issues with CSS that are now compatible with modern browsers
- Added mobile compatibility tags and standardized to webfonts for mobile compatibility
- Fixed anything that used adobe/macromedia flash
- Removed IE6 compatibility layer
- Updated copyright and license information
- Got rid of the silly glossary functionality
- Made sure all links now use https instead of http
- Added my email address to the contact page
- Overhauled the search page
15
« on: February 16, 2024, 01:03:32 am »
Go has been around for quite a while now and has had a lot of time to grow and mature, and it is really quite a lovely language. Watching it develop since its inception has been a delight. Its native concurrency is best in the industry, it is designed for things to be super-clean and standardized, and it compiles really fast. The support for it in IntelliJ/GoLand is top-notch and makes it really a blast to work in. There are of course a lot of design decisions in the language that could be debated, like how setting a variable has to be a top level statement that cannot be embedded in other statements, how there is no “real” class based system, and how their specific name casing conventions are actually built into the language and enforced. However, I understand the reason for all of these decisions and feel that the designers of the language were justified in the decisions they made for the reasons they give. There are at least 6 problems I’ve run into a lot recently though that I feel could be better. I would love to jump into the source of the language and see about making these fixes myself, but the GoLang team has a history of completely ignoring outsiders, and it would be highly unlikely that they would accept anything I submitted, especially since a number of these things would require very long and complicated proposals to even start looking at. Go may be an open source language, but it is not open development. These first 3 would be additions to the language that would not break anything, which is in line with their promise of never breaking anything. 1) Interface with constraints elements cannot be used as variable types. Example: type intish interface{ int64 | uint64 } func adder[T intish](val T) T { return val + 1 } //This is allowed var val intish //Error: cannot use type intish outside a type constraint: interface contains type constraints
IntelliJ inspection Error: Interface includes constraint elements '...', can only be used in type parameters It wouldn’t be a big change for the compiler and runtime to be able to enforce these types of constraints when typecasting and I really wish the language had this. This is the type of thing TypeScript does beautifully since it is actually part of its native design and functioning. 2) Member functions (methods with receivers) cannot have generics type foobar int func (*foo) bar[T any](val T) {} //Method cannot have type parameters
3) []generic cannot be typecast to []anyThere is no real reason that an array containing interfaces shouldn’t be convertable via typecast to another array of interface types as long as they are compatible. type testAny any startVar := make([]testAny, 3) endVar := []any(startVar) //cannot convert startVar (variable of type []testAny) to type []any
This next one is an unfortunate consequence of the language design and would not be easily fixable. 4) Import cycle not allowedThis means there cannot be circular dependencies. If package A includes package B, then package B cannot include package A. This often wasn’t a problem with languages like C since the headers were independent of the implementations, but because packages in Go do not have headers, and their package types cannot be used as method receivers in other packages, this one is just not possible. And it can be quite limiting.
This next one is a bug in either the language or the documentation, and I consider it to be a security problem, but the go team does not seem to care about it. See https://github.com/golang/go/issues/65201 [Edit on 2024-04-10] Oh wow! they fixed it! 5) sql.RawBytes modifies the current buffers instead of setting new buffers like the documentation says.All the documentation says about RawBytes is “RawBytes is a byte slice that holds a reference to memory owned by the database itself. After a Rows.Scan into a RawBytes, the slice is only valid until the next call to Rows.Next, Rows.Scan, or Rows.Close.” If the value you are reading is a []byte/string then reading into a RawBytes works as expected. However, if it is anything else, like an int, it reads into the buffer that RawBytes already holds. This can lead to buffer injections and other really nasty bugs. For example: db.Exec(`CREATE TEMPORARY TABLE goTest (i int NOT NULL, str varchar(10)) ENGINE=MEMORY`) db.Exec(`INSERT INTO goTest VALUES (?, ?)`, 6, "foobar") var scanIn sql.RawBytes rows, _ := db.Query(`SELECT str FROM goTest WHERE i=?`, 6) rows.Next() rows.Scan(&scanIn) //Do something with scanIn rows.Close() rows, _ = db.Query(`SELECT i FROM goTest WHERE i=?`, 6) rows.Next() rows.Scan(&scanIn) //This corrupts the internal sql driver buffer since it reads an int into the pointer we received earlier
This final one has been an annoyance of mine since day 1 of working with Go, and it still annoys the hell out of me. There could be ways to fix it without breaking things, but I cannot think of any truly elegant solutions. 6) Short variable declaration can’t handle setting both new and already existing variablesThis is the most common pattern you’ll see in Go by far: var data string if _data, err := someFunc(); err != nil { fmt.Println(err) } else { data = _data }
You can either do it this way, or also declare err in the outer scope, thereby polluting the scope with unneeded variables. There is no way to set both the temporary error variable and also set the outer data variable in 1 line. One non-breaking hack would be to add a symbol before variables you wanted to set in the outer scope. Example: var data string if +data, err := someFunc(); err != nil { fmt.Println(err) }
|
|