feat: display evidence details and organize imports

This commit is contained in:
grayTerminal-sh 2026-07-20 12:39:36 +02:00
parent 5d87b35242
commit a4765e8de2
5 changed files with 1115 additions and 53 deletions

View file

@ -11,6 +11,7 @@
#include "core/investigation_node.h"
#include "core/task_manager.h"
#include "widgets/evidence_category_model.h"
#include "models/evidence_record.h"
#include <gtk/gtk.h>
@ -187,6 +188,19 @@ void main_window_set_selected_node(
const InvestigationNode *node
);
/**
* @brief Affiche la fiche de la preuve sélectionnée.
*
* MainWindow ne conserve pas le EvidenceRecord.
*
* @param main_window Fenêtre principale.
* @param evidence_record Preuve sélectionnée, ou NULL.
*/
void main_window_set_selected_evidence(
MainWindow *main_window,
const EvidenceRecord *evidence_record
);
/**
* @brief Met à jour le texte de la barre d'état.
*

View file

@ -8,6 +8,7 @@
#include "core/investigation_node.h"
#include "models/evidence_record.h"
#include <gtk/gtk.h>
@ -56,6 +57,22 @@ void workspace_set_selected_node(
const InvestigationNode *node
);
/**
* @brief Affiche la fiche détaillée d'une preuve.
*
* Les valeurs sont immédiatement copiées par les widgets GTK.
* Le Workspace ne conserve pas le pointeur EvidenceRecord.
*
* Passer NULL restaure la page d'accueil.
*
* @param workspace Zone de travail à mettre à jour.
* @param evidence_record Preuve sélectionnée, ou NULL.
*/
void workspace_set_selected_evidence(
Workspace *workspace,
const EvidenceRecord *evidence_record
);
/**
* @brief Libère la structure d'encapsulation de la zone de travail.
*

View file

@ -36,12 +36,6 @@
*/
#define APPLICATION_ID "com.labfytools.investigation"
/**
* @brief Dossier physique recevant les documents importés.
*/
#define APPLICATION_EVIDENCE_DIRECTORY \
"01_Preuves_Originales/Documents"
/**
* @brief Erreurs produites lors du chargement d'une enquête.
*/
@ -60,6 +54,12 @@ typedef enum
#define APPLICATION_OPEN_ERROR \
application_open_error_quark()
/**
* @brief Dossier racine contenant les preuves originales.
*/
#define APPLICATION_EVIDENCE_ROOT_DIRECTORY \
"01_Preuves_Originales"
/**
* @brief Retourne le domaine d'erreur du chargement d'une enquête.
*
@ -1511,6 +1511,15 @@ static void application_on_evidence_import_completed(
GError *error = NULL;
GError *evidence_refresh_error =
NULL;
gboolean tree_refreshed =
FALSE;
gboolean evidence_refreshed =
FALSE;
if (task == NULL ||
context == NULL)
{
@ -1560,56 +1569,117 @@ static void application_on_evidence_import_completed(
evidence_record
);
if (application_session_matches_root(
application,
context->investigation_root_path
))
{
if (application_refresh_investigation_tree(
/*
* L'utilisateur peut avoir changé d'enquête pendant
* l'exécution asynchrone de l'import.
*
* Dans ce cas, la preuve reste correctement importée dans
* l'ancienne enquête, mais l'interface actuelle ne doit pas
* être rafraîchie avec ses données.
*/
if (application_session_matches_root(
application,
context->investigation_root_path
))
{
status_message =
g_strdup_printf(
"Preuve importée : %s",
original_name != NULL
? original_name
: "(nom indisponible)"
);
}
else
{
/*
* Limport reste valide même si le rafraîchissement visuel
* échoue. Le fichier et la ligne SQLite ne doivent surtout
* pas être présentés comme un échec.
*/
status_message =
g_strdup_printf(
"Preuve importée : %s — "
"arborescence non actualisée",
original_name != NULL
? original_name
: "(nom indisponible)"
tree_refreshed =
application_refresh_investigation_tree(
application,
context->investigation_root_path
);
g_warning(
"La preuve a été importée, mais larborescence de '%s' "
"na pas pu être reconstruite.",
context->investigation_root_path
evidence_refreshed =
application_refresh_evidence_models(
application,
&evidence_refresh_error
);
if (!tree_refreshed)
{
g_warning(
"La preuve a été importée, mais l'arborescence de '%s' "
"n'a pas pu être reconstruite.",
context->investigation_root_path
);
}
if (!evidence_refreshed)
{
g_warning(
"La preuve a été importée, mais la liste des preuves "
"n'a pas pu être actualisée : %s",
evidence_refresh_error != NULL
? evidence_refresh_error->message
: "erreur inconnue"
);
}
if (tree_refreshed &&
evidence_refreshed)
{
status_message =
g_strdup_printf(
"Preuve importée : %s",
original_name != NULL &&
original_name[0] != '\0'
? original_name
: "(nom indisponible)"
);
}
else if (!tree_refreshed &&
evidence_refreshed)
{
status_message =
g_strdup_printf(
"Preuve importée : %s — "
"arborescence non actualisée",
original_name != NULL &&
original_name[0] != '\0'
? original_name
: "(nom indisponible)"
);
}
else if (tree_refreshed &&
!evidence_refreshed)
{
status_message =
g_strdup_printf(
"Preuve importée : %s — "
"liste des preuves non actualisée",
original_name != NULL &&
original_name[0] != '\0'
? original_name
: "(nom indisponible)"
);
}
else
{
status_message =
g_strdup_printf(
"Preuve importée : %s — "
"interface non actualisée",
original_name != NULL &&
original_name[0] != '\0'
? original_name
: "(nom indisponible)"
);
}
main_window_set_status(
application->main_window,
status_message
);
g_clear_error(
&evidence_refresh_error
);
}
main_window_set_status(
application->main_window,
status_message
);
}
g_message(
"Preuve importée dans '%s' : %s",
context->investigation_root_path,
original_name != NULL
original_name != NULL &&
original_name[0] != '\0'
? original_name
: "(nom indisponible)"
);
@ -1663,6 +1733,77 @@ static void application_on_evidence_import_completed(
);
}
/**
* @brief Retourne le dossier de stockage associé à un type de preuve.
*
* La chaîne retournée est statique et ne doit pas être libérée.
*/
static const char *application_get_evidence_subdirectory(
const char *type_identifier
)
{
if (g_strcmp0(
type_identifier,
"screenshot"
) == 0)
{
return "Captures_Ecran";
}
if (g_strcmp0(
type_identifier,
"photo"
) == 0)
{
return "Photos";
}
if (g_strcmp0(
type_identifier,
"video"
) == 0)
{
return "Videos";
}
if (g_strcmp0(
type_identifier,
"email"
) == 0)
{
return "Emails";
}
if (g_strcmp0(
type_identifier,
"audio"
) == 0)
{
return "Audios";
}
if (g_strcmp0(
type_identifier,
"archive"
) == 0)
{
return "Archives";
}
if (g_strcmp0(
type_identifier,
"conversation"
) == 0)
{
return "Conversations";
}
/*
* Documents, textes, autres types et types inconnus.
*/
return "Documents";
}
/**
* @brief Prépare et démarre limport asynchrone dun fichier.
*/
@ -1680,6 +1821,10 @@ static void application_start_evidence_import(
const char *root_path = NULL;
const char *database_path = NULL;
const char *evidence_subdirectory = NULL;
char *relative_directory = NULL;
EvidenceImportTaskRequest request =
{0};
@ -1748,11 +1893,33 @@ static void application_start_evidence_import(
return;
}
evidence_subdirectory =
application_get_evidence_subdirectory(
type_identifier
);
relative_directory =
g_build_filename(
APPLICATION_EVIDENCE_ROOT_DIRECTORY,
evidence_subdirectory,
NULL
);
if (relative_directory == NULL)
{
application_present_error(
application,
"Import impossible",
"Impossible de construire le chemin relatif de la preuve."
);
return;
}
destination_directory =
g_build_filename(
root_path,
"01_Preuves_Originales",
"Documents",
relative_directory,
NULL
);
@ -1764,6 +1931,10 @@ static void application_start_evidence_import(
"Impossible de construire le dossier de destination."
);
g_free(
relative_directory
);
return;
}
@ -1859,7 +2030,7 @@ static void application_start_evidence_import(
destination_directory;
request.relative_directory =
APPLICATION_EVIDENCE_DIRECTORY;
relative_directory;
request.type_identifier =
type_identifier;
@ -1908,6 +2079,10 @@ static void application_start_evidence_import(
destination_directory
);
g_free(
relative_directory
);
return;
}
@ -1952,6 +2127,10 @@ static void application_start_evidence_import(
g_free(
destination_directory
);
g_free(
relative_directory
);
}
/**
@ -2368,6 +2547,224 @@ static void application_on_tree_node_selected(
}
}
/**
* @brief Traite la sélection d'une preuve dans la Sidebar.
*
* L'identifiant est emprunté uniquement pendant cet appel.
* La preuve complète est relue depuis SQLite avant d'être transmise
* au Workspace.
*
* @param evidence_identifier UUID de la preuve, ou NULL.
* @param user_data Pointeur vers Application.
*/
static void application_on_evidence_selected(
const char *evidence_identifier,
gpointer user_data
)
{
Application *application =
user_data;
Database *database =
NULL;
EvidenceDao *evidence_dao =
NULL;
EvidenceRecord *evidence_record =
NULL;
const char *original_name =
NULL;
char *status_message =
NULL;
GError *error =
NULL;
if (application == NULL ||
application->main_window == NULL)
{
return;
}
/*
* Une catégorie ou une sélection vide transmet NULL.
*/
if (evidence_identifier == NULL ||
evidence_identifier[0] == '\0')
{
main_window_set_selected_evidence(
application->main_window,
NULL
);
return;
}
if (application->session == NULL)
{
main_window_set_selected_evidence(
application->main_window,
NULL
);
return;
}
database =
investigation_session_get_database(
application->session
);
if (database == NULL)
{
main_window_set_selected_evidence(
application->main_window,
NULL
);
application_present_error(
application,
"Preuve indisponible",
"La session ne fournit aucune connexion SQLite."
);
return;
}
evidence_dao =
evidence_dao_new(
database,
&error
);
if (evidence_dao == NULL)
{
g_warning(
"Impossible de créer le DAO des preuves : %s",
error != NULL
? error->message
: "erreur inconnue"
);
main_window_set_selected_evidence(
application->main_window,
NULL
);
application_present_error(
application,
"Preuve indisponible",
error != NULL
? error->message
: "Impossible d'accéder aux preuves enregistrées."
);
g_clear_error(
&error
);
return;
}
evidence_record =
evidence_dao_find_by_identifier(
evidence_dao,
evidence_identifier,
&error
);
evidence_dao_free(
evidence_dao
);
if (error != NULL)
{
g_warning(
"Impossible de charger la preuve '%s' : %s",
evidence_identifier,
error->message
);
main_window_set_selected_evidence(
application->main_window,
NULL
);
application_present_error(
application,
"Preuve indisponible",
error->message
);
g_clear_error(
&error
);
return;
}
/*
* Une preuve absente retourne NULL sans erreur.
*/
if (evidence_record == NULL)
{
g_warning(
"La preuve '%s' n'existe plus dans la base.",
evidence_identifier
);
main_window_set_selected_evidence(
application->main_window,
NULL
);
main_window_set_status(
application->main_window,
"La preuve sélectionnée est introuvable."
);
return;
}
/*
* Workspace copie immédiatement les valeurs dans ses widgets GTK.
*/
main_window_set_selected_evidence(
application->main_window,
evidence_record
);
original_name =
evidence_record_get_original_name(
evidence_record
);
status_message =
g_strdup_printf(
"Preuve sélectionnée : %s",
original_name != NULL &&
original_name[0] != '\0'
? original_name
: "(sans nom)"
);
main_window_set_status(
application->main_window,
status_message
);
g_free(
status_message
);
evidence_record_free(
evidence_record
);
}
/**
* @brief Ferme proprement l'application.
*
@ -2446,6 +2843,12 @@ static void application_on_activate(
application
);
main_window_set_evidence_selection_callback(
application->main_window,
application_on_evidence_selected,
application
);
main_window_set_new_investigation_callback(
application->main_window,
application_on_new_investigation_requested,

View file

@ -368,11 +368,6 @@ MainWindow *main_window_new(
FALSE
);
main_window->quit_button =
gtk_button_new_with_label(
"Quitter"
);
gtk_box_append(
GTK_BOX(main_window->action_bar),
main_window->new_investigation_button
@ -978,6 +973,22 @@ void main_window_set_selected_node(
);
}
void main_window_set_selected_evidence(
MainWindow *main_window,
const EvidenceRecord *evidence_record
)
{
if (main_window == NULL)
{
return;
}
workspace_set_selected_evidence(
main_window->workspace,
evidence_record
);
}
void main_window_free(
MainWindow *main_window
)
@ -1017,6 +1028,17 @@ void main_window_free(
sidebar_set_tree_model(
main_window->sidebar,
NULL
);
sidebar_set_evidence_selection_callback(
main_window->sidebar,
NULL,
NULL
);
sidebar_set_evidence_model(
main_window->sidebar,
NULL
);
}

View file

@ -9,7 +9,8 @@
#define WORKSPACE_PAGE_WELCOME "welcome"
#define WORKSPACE_PAGE_NODE_INFORMATION "node-information"
#define WORKSPACE_PAGE_EVIDENCE_INFORMATION \
"evidence-information"
/**
* @struct Workspace
* @brief Représentation interne de la zone de travail.
@ -25,6 +26,22 @@ struct Workspace
GtkWidget *welcome_instruction_label;
GtkWidget *node_page;
GtkWidget *evidence_page;
GtkWidget *evidence_name_label;
GtkWidget *evidence_type_label;
GtkWidget *evidence_integrity_label;
GtkWidget *evidence_size_label;
GtkWidget *evidence_imported_at_label;
GtkWidget *evidence_collected_at_label;
GtkWidget *evidence_source_label;
GtkWidget *evidence_description_label;
GtkWidget *evidence_relative_path_label;
GtkWidget *evidence_internal_name_label;
GtkWidget *evidence_identifier_label;
GtkWidget *evidence_sha256_label;
GtkWidget *node_name_label;
GtkWidget *node_path_label;
GtkWidget *node_type_label;
@ -32,6 +49,173 @@ struct Workspace
GtkWidget *node_children_label;
};
/**
* @brief Ajoute une ligne de métadonnée dans la grille.
*
* @return Label GTK destiné à recevoir la valeur.
*/
static GtkWidget *workspace_add_evidence_field(
GtkGrid *grid,
gint row,
const char *title
)
{
GtkWidget *title_label = NULL;
GtkWidget *value_label = NULL;
if (grid == NULL ||
title == NULL ||
title[0] == '\0')
{
return NULL;
}
title_label =
gtk_label_new(
title
);
value_label =
gtk_label_new(
NULL
);
if (title_label == NULL ||
value_label == NULL)
{
g_clear_object(
&value_label
);
g_clear_object(
&title_label
);
return NULL;
}
gtk_label_set_xalign(
GTK_LABEL(title_label),
0.0F
);
gtk_widget_set_valign(
title_label,
GTK_ALIGN_START
);
gtk_widget_add_css_class(
title_label,
"dim-label"
);
gtk_label_set_xalign(
GTK_LABEL(value_label),
0.0F
);
gtk_label_set_wrap(
GTK_LABEL(value_label),
TRUE
);
gtk_label_set_wrap_mode(
GTK_LABEL(value_label),
PANGO_WRAP_WORD_CHAR
);
gtk_label_set_selectable(
GTK_LABEL(value_label),
TRUE
);
gtk_widget_set_hexpand(
value_label,
TRUE
);
gtk_widget_set_halign(
value_label,
GTK_ALIGN_FILL
);
gtk_widget_set_valign(
value_label,
GTK_ALIGN_START
);
gtk_grid_attach(
grid,
title_label,
0,
row,
1,
1
);
gtk_grid_attach(
grid,
value_label,
1,
row,
1,
1
);
return value_label;
}
/**
* @brief Définit une valeur avec un texte de remplacement.
*/
static void workspace_set_field_text(
GtkWidget *label,
const char *value,
const char *fallback
)
{
if (label == NULL)
{
return;
}
gtk_label_set_text(
GTK_LABEL(label),
value != NULL && value[0] != '\0'
? value
: fallback
);
}
/**
* @brief Convertit un statut d'intégrité en texte utilisateur.
*/
static const char *workspace_get_integrity_status_text(
EvidenceIntegrityStatus integrity_status
)
{
switch (integrity_status)
{
case EVIDENCE_INTEGRITY_STATUS_UNKNOWN:
return "Non vérifiée";
case EVIDENCE_INTEGRITY_STATUS_VALID:
return "Valide";
case EVIDENCE_INTEGRITY_STATUS_MISSING:
return "Fichier absent";
case EVIDENCE_INTEGRITY_STATUS_MODIFIED:
return "Fichier modifié";
case EVIDENCE_INTEGRITY_STATUS_ERROR:
return "Erreur de vérification";
default:
return "Statut inconnu";
}
}
/**
* @brief Affiche la page d'accueil.
*/
@ -52,6 +236,10 @@ static void workspace_show_welcome(
Workspace *workspace_new(void)
{
GtkWidget *evidence_content = NULL;
GtkWidget *evidence_separator = NULL;
GtkWidget *evidence_grid = NULL;
Workspace *workspace = NULL;
workspace = g_new0(Workspace, 1);
@ -233,6 +421,270 @@ Workspace *workspace_new(void)
WORKSPACE_PAGE_NODE_INFORMATION
);
/*
* Page détaillée d'une preuve.
*/
workspace->evidence_page =
gtk_scrolled_window_new();
if (workspace->evidence_page == NULL)
{
workspace_free(
workspace
);
return NULL;
}
gtk_widget_set_hexpand(
workspace->evidence_page,
TRUE
);
gtk_widget_set_vexpand(
workspace->evidence_page,
TRUE
);
gtk_scrolled_window_set_policy(
GTK_SCROLLED_WINDOW(
workspace->evidence_page
),
GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC
);
evidence_content =
gtk_box_new(
GTK_ORIENTATION_VERTICAL,
16
);
if (evidence_content == NULL)
{
workspace_free(
workspace
);
return NULL;
}
gtk_widget_set_margin_start(
evidence_content,
24
);
gtk_widget_set_margin_end(
evidence_content,
24
);
gtk_widget_set_margin_top(
evidence_content,
24
);
gtk_widget_set_margin_bottom(
evidence_content,
24
);
workspace->evidence_name_label =
gtk_label_new(
NULL
);
if (workspace->evidence_name_label == NULL)
{
workspace_free(
workspace
);
return NULL;
}
gtk_label_set_xalign(
GTK_LABEL(
workspace->evidence_name_label
),
0.0F
);
gtk_label_set_wrap(
GTK_LABEL(
workspace->evidence_name_label
),
TRUE
);
gtk_label_set_selectable(
GTK_LABEL(
workspace->evidence_name_label
),
TRUE
);
gtk_widget_add_css_class(
workspace->evidence_name_label,
"title-2"
);
gtk_box_append(
GTK_BOX(evidence_content),
workspace->evidence_name_label
);
evidence_separator =
gtk_separator_new(
GTK_ORIENTATION_HORIZONTAL
);
gtk_box_append(
GTK_BOX(evidence_content),
evidence_separator
);
evidence_grid =
gtk_grid_new();
if (evidence_grid == NULL)
{
workspace_free(
workspace
);
return NULL;
}
gtk_grid_set_column_spacing(
GTK_GRID(evidence_grid),
24
);
gtk_grid_set_row_spacing(
GTK_GRID(evidence_grid),
12
);
gtk_widget_set_hexpand(
evidence_grid,
TRUE
);
workspace->evidence_type_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
0,
"Type"
);
workspace->evidence_integrity_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
1,
"Intégrité"
);
workspace->evidence_size_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
2,
"Taille"
);
workspace->evidence_imported_at_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
3,
"Importée le"
);
workspace->evidence_collected_at_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
4,
"Collectée le"
);
workspace->evidence_source_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
5,
"Source"
);
workspace->evidence_description_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
6,
"Description"
);
workspace->evidence_relative_path_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
7,
"Chemin relatif"
);
workspace->evidence_internal_name_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
8,
"Nom interne"
);
workspace->evidence_identifier_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
9,
"Identifiant"
);
workspace->evidence_sha256_label =
workspace_add_evidence_field(
GTK_GRID(evidence_grid),
10,
"SHA-256"
);
if (workspace->evidence_type_label == NULL ||
workspace->evidence_integrity_label == NULL ||
workspace->evidence_size_label == NULL ||
workspace->evidence_imported_at_label == NULL ||
workspace->evidence_collected_at_label == NULL ||
workspace->evidence_source_label == NULL ||
workspace->evidence_description_label == NULL ||
workspace->evidence_relative_path_label == NULL ||
workspace->evidence_internal_name_label == NULL ||
workspace->evidence_identifier_label == NULL ||
workspace->evidence_sha256_label == NULL)
{
workspace_free(
workspace
);
return NULL;
}
gtk_box_append(
GTK_BOX(evidence_content),
evidence_grid
);
gtk_scrolled_window_set_child(
GTK_SCROLLED_WINDOW(
workspace->evidence_page
),
evidence_content
);
gtk_stack_add_named(
GTK_STACK(workspace->stack),
workspace->evidence_page,
WORKSPACE_PAGE_EVIDENCE_INFORMATION
);
workspace_show_welcome(workspace);
return workspace;
@ -367,6 +819,160 @@ void workspace_set_selected_node(
g_free(parent_text);
}
void workspace_set_selected_evidence(
Workspace *workspace,
const EvidenceRecord *evidence_record
)
{
char *human_size = NULL;
char *size_text = NULL;
guint64 size_bytes = 0;
if (workspace == NULL)
{
return;
}
if (evidence_record == NULL)
{
workspace_show_welcome(
workspace
);
return;
}
size_bytes =
evidence_record_get_size_bytes(
evidence_record
);
human_size =
g_format_size_full(
size_bytes,
G_FORMAT_SIZE_IEC_UNITS
);
size_text =
g_strdup_printf(
"%s (%" G_GUINT64_FORMAT " octets)",
human_size != NULL
? human_size
: "taille inconnue",
size_bytes
);
workspace_set_field_text(
workspace->evidence_name_label,
evidence_record_get_original_name(
evidence_record
),
"(sans nom)"
);
workspace_set_field_text(
workspace->evidence_type_label,
evidence_record_get_type_identifier(
evidence_record
),
"(type inconnu)"
);
workspace_set_field_text(
workspace->evidence_integrity_label,
workspace_get_integrity_status_text(
evidence_record_get_integrity_status(
evidence_record
)
),
"Statut inconnu"
);
workspace_set_field_text(
workspace->evidence_size_label,
size_text,
"Taille inconnue"
);
workspace_set_field_text(
workspace->evidence_imported_at_label,
evidence_record_get_imported_at(
evidence_record
),
"(date inconnue)"
);
workspace_set_field_text(
workspace->evidence_collected_at_label,
evidence_record_get_collected_at(
evidence_record
),
"Non renseignée"
);
workspace_set_field_text(
workspace->evidence_source_label,
evidence_record_get_source(
evidence_record
),
"Non renseignée"
);
workspace_set_field_text(
workspace->evidence_description_label,
evidence_record_get_description(
evidence_record
),
"Non renseignée"
);
workspace_set_field_text(
workspace->evidence_relative_path_label,
evidence_record_get_relative_path(
evidence_record
),
"(chemin inconnu)"
);
workspace_set_field_text(
workspace->evidence_internal_name_label,
evidence_record_get_internal_name(
evidence_record
),
"(nom interne inconnu)"
);
workspace_set_field_text(
workspace->evidence_identifier_label,
evidence_record_get_identifier(
evidence_record
),
"(identifiant inconnu)"
);
workspace_set_field_text(
workspace->evidence_sha256_label,
evidence_record_get_sha256(
evidence_record
),
"(empreinte inconnue)"
);
gtk_stack_set_visible_child_name(
GTK_STACK(workspace->stack),
WORKSPACE_PAGE_EVIDENCE_INFORMATION
);
g_free(
size_text
);
g_free(
human_size
);
}
void workspace_free(Workspace *workspace)
{
if (workspace == NULL)